diff --git a/.github/hooks/pre-commit b/.github/hooks/pre-commit deleted file mode 100755 index 3d451ccc..00000000 --- a/.github/hooks/pre-commit +++ /dev/null @@ -1,11 +0,0 @@ -#!/bin/bash -# .git/hooks/pre-commit - -echo "๐Ÿ”จ Gradle ๋นŒ๋“œ ๊ฒ€์ฆโ€ฆ" - -if ! ./gradlew clean build --no-daemon; then - echo "โŒ ๋นŒ๋“œ ์‹คํŒจโ€”์ปค๋ฐ‹ ์ค‘๋‹จ" - exit 1 -fi - -echo "โœ… ๋นŒ๋“œ ์„ฑ๊ณต" diff --git a/.github/hooks/pre-push b/.github/hooks/pre-push new file mode 100755 index 00000000..8308eb18 --- /dev/null +++ b/.github/hooks/pre-push @@ -0,0 +1,11 @@ +#!/bin/bash +# .git/hooks/pre-push + +echo "๐Ÿ”จ Gradle ๋นŒ๋“œ ๊ฒ€์ฆ (push ์ „ ์‹คํ–‰)โ€ฆ" + +if ! ./gradlew clean build --no-daemon; then + echo "โŒ ๋นŒ๋“œ ์‹คํŒจโ€”ํ‘ธ์‹œ ์ค‘๋‹จ" + exit 1 +fi + +echo "โœ… ๋นŒ๋“œ ์„ฑ๊ณตโ€”ํ‘ธ์‹œ ์ง„ํ–‰" diff --git a/buildSrc/src/main/kotlin/Dependencies.kt b/buildSrc/src/main/kotlin/Dependencies.kt index 1fb4dc95..4eef9734 100644 --- a/buildSrc/src/main/kotlin/Dependencies.kt +++ b/buildSrc/src/main/kotlin/Dependencies.kt @@ -12,6 +12,12 @@ object Dependencies { //jexl const val APACHE_COMMONS_JEXL = "org.apache.commons:commons-jexl3:${DependencyVersions.APACHE_COMMONS_JEXL_VERSION}" + //kotlinx serialization + const val KOTLINX_SERIALIZATION_JSON = "org.jetbrains.kotlinx:kotlinx-serialization-json:${DependencyVersions.KOTLINX_SERIALIZATION_VERSION}" + + //kotlinx coroutines + const val KOTLINX_COROUTINES_CORE = "org.jetbrains.kotlinx:kotlinx-coroutines-core:${DependencyVersions.KOTLINX_COROUTINES_VERSION}" + //junit const val JUNIT = "org.jetbrains.kotlin:kotlin-test-junit5" const val JUNIT_PLATFORM_LAUNCHER = "org.junit.platform:junit-platform-launcher" diff --git a/buildSrc/src/main/kotlin/DependencyVersions.kt b/buildSrc/src/main/kotlin/DependencyVersions.kt index 738177f8..a229be14 100644 --- a/buildSrc/src/main/kotlin/DependencyVersions.kt +++ b/buildSrc/src/main/kotlin/DependencyVersions.kt @@ -1,4 +1,10 @@ object DependencyVersions { // JEXL const val APACHE_COMMONS_JEXL_VERSION = "3.5.0" + + // Kotlinx Serialization + const val KOTLINX_SERIALIZATION_VERSION = "1.6.3" + + // Kotlinx Coroutines + const val KOTLINX_COROUTINES_VERSION = "1.8.1" } \ No newline at end of file diff --git a/buildSrc/src/main/kotlin/Plugins.kt b/buildSrc/src/main/kotlin/Plugins.kt index 96482103..3532b5ef 100644 --- a/buildSrc/src/main/kotlin/Plugins.kt +++ b/buildSrc/src/main/kotlin/Plugins.kt @@ -2,6 +2,8 @@ object Plugins { const val KOTLIN_JVM = "jvm" const val JETBRAINS_KOTLIN_JVM = "org.jetbrains.kotlin.jvm" const val KOTLIN_SPRING = "plugin.spring" + const val KOTLIN_SERIALIZATION = "plugin.serialization" + const val JETBRAINS_KOTLIN_SERIALIZATION = "org.jetbrains.kotlin.plugin.serialization" const val SPRING_BOOT = "org.springframework.boot" const val SPRING_DEPENDENCY_MANAGEMENT = "io.spring.dependency-management" const val KTLINT = "org.jlleitschuh.gradle.ktlint" diff --git a/casper-application-domain/build.gradle.kts b/casper-application-domain/build.gradle.kts index 7c81cef0..87c58d05 100644 --- a/casper-application-domain/build.gradle.kts +++ b/casper-application-domain/build.gradle.kts @@ -1,10 +1,14 @@ plugins { kotlin(Plugins.KOTLIN_JVM) version PluginVersions.KOTLIN_VERSION + kotlin(Plugins.KOTLIN_SERIALIZATION) version PluginVersions.KOTLIN_VERSION } version = Projects.APPLICATION_DOMAIN_VERSION dependencies { + implementation(Dependencies.KOTLINX_SERIALIZATION_JSON) + implementation(Dependencies.KOTLINX_COROUTINES_CORE) + testImplementation(Dependencies.JUNIT) testRuntimeOnly(Dependencies.JUNIT_PLATFORM_LAUNCHER) testImplementation(Dependencies.KOTLIN_TEST) diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/aggregates/ExpressionAST.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/aggregates/ExpressionAST.kt new file mode 100644 index 00000000..4c6b7f70 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/aggregates/ExpressionAST.kt @@ -0,0 +1,524 @@ +package hs.kr.entrydsm.domain.ast.aggregates + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.IfNode +import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode +import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.entities.ArgumentsNode +import hs.kr.entrydsm.domain.ast.entities.NumberNode +import hs.kr.entrydsm.domain.ast.entities.BooleanNode +import hs.kr.entrydsm.domain.ast.entities.VariableNode +import hs.kr.entrydsm.domain.ast.events.DomainEvents +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.services.TreeTraverser +import hs.kr.entrydsm.domain.ast.services.TreeOptimizer +import hs.kr.entrydsm.domain.ast.factories.ASTNodeFactory +import hs.kr.entrydsm.domain.ast.specifications.ASTValiditySpec +import hs.kr.entrydsm.domain.ast.specifications.NodeStructureSpec +import hs.kr.entrydsm.domain.ast.values.NodeSize +import hs.kr.entrydsm.domain.ast.values.TreeDepth +import hs.kr.entrydsm.domain.ast.values.OptimizationLevel +import hs.kr.entrydsm.domain.ast.values.ASTValidationResult +import hs.kr.entrydsm.domain.ast.values.ASTOptimizationResult +import hs.kr.entrydsm.domain.ast.values.TreeStatistics +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import java.time.LocalDateTime +import java.util.* + +/** + * ํ‘œํ˜„์‹ AST๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ์ž…๋‹ˆ๋‹ค. + * + * AST ํŠธ๋ฆฌ์˜ ์ƒ์„ฑ, ์ˆ˜์ •, ๊ฒ€์ฆ, ์ตœ์ ํ™” ๋“ฑ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ + * ์บก์Аํ™”ํ•˜๊ณ  ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Aggregate(context = "ast") +class ExpressionAST private constructor( + val id: String, + private var root: ASTNode, + private val traverser: TreeTraverser = TreeTraverser(), + private val optimizer: TreeOptimizer = TreeOptimizer(), + private val factory: ASTNodeFactory = ASTNodeFactory(), + private val validitySpec: ASTValiditySpec = ASTValiditySpec(), + private val structureSpec: NodeStructureSpec = NodeStructureSpec(), + private val createdAt: LocalDateTime = LocalDateTime.now(), + private var lastModifiedAt: LocalDateTime = LocalDateTime.now(), + private var optimizationLevel: OptimizationLevel = OptimizationLevel.NONE, + private var isValidated: Boolean = false, + private var validationResult: ASTValidationResult? = null +) { + + // ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ + private val domainEvents = mutableListOf() + + /** + * ๋ฃจํŠธ ๋…ธ๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getRoot(): ASTNode = root + + + /** + * ์ƒ์„ฑ ์‹œ๊ฐ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getCreatedAt(): LocalDateTime = createdAt + + /** + * ๋งˆ์ง€๋ง‰ ์ˆ˜์ • ์‹œ๊ฐ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getLastModifiedAt(): LocalDateTime = lastModifiedAt + + /** + * ์ตœ์ ํ™” ๋ ˆ๋ฒจ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getOptimizationLevel(): OptimizationLevel = optimizationLevel + + /** + * ๊ฒ€์ฆ ์—ฌ๋ถ€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun isValidated(): Boolean = isValidated + + /** + * ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getValidationResult(): ASTValidationResult? = validationResult + + /** + * ํŠธ๋ฆฌ ํฌ๊ธฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getSize(): NodeSize = NodeSize.of(root.getSize()) + + /** + * ํŠธ๋ฆฌ ๊นŠ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getDepth(): TreeDepth = TreeDepth.of(root.getDepth()) + + /** + * ๋ณ€์ˆ˜ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getVariables(): Set = root.getVariables() + + /** + * ๋ฃจํŠธ ๋…ธ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun setRoot(newRoot: ASTNode) { + if(!validitySpec.isSatisfiedBy(newRoot)) { + val reason = validitySpec.getWhyNotSatisfied(newRoot) + throw ASTException.invalidRootNode(reason) + } + + val oldRoot = this.root + this.root = newRoot + this.lastModifiedAt = LocalDateTime.now() + this.isValidated = false + this.validationResult = null + + // ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + addDomainEvent(mapOf( + "eventType" to DomainEvents.AST_MODIFIED, + "aggregateId" to id, + "aggregateType" to DomainEvents.EXPRESSION_AST, + "payload" to mapOf( + "oldRoot" to oldRoot.toString(), + "newRoot" to newRoot.toString(), + "modifiedAt" to LocalDateTime.now().toString() + ) + )) + } + + /** + * AST๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + fun validate(): ASTValidationResult { + val violations = mutableListOf() + + // ์œ ํšจ์„ฑ ๊ฒ€์ฆ + if (!validitySpec.isSatisfiedBy(root)) { + val reason = validitySpec.getWhyNotSatisfied(root) + throw ASTException.invalidRootNode(reason) + } + + // ๊ตฌ์กฐ ๊ฒ€์ฆ + if (!structureSpec.isSatisfiedBy(root)) { + val reason = structureSpec.getWhyNotSatisfied(root) + throw ASTException.invalidNodeStructure(root.toString(), reason) + } + + // ํฌ๊ธฐ ์ œํ•œ ๊ฒ€์ฆ + if (getSize().isAtLimit()) { + throw ASTException.sizeLimitExceeded() + } + + // ๊นŠ์ด ์ œํ•œ ๊ฒ€์ฆ + if (getDepth().isAtLimit()) { + throw ASTException.depthLimitExceeded() + } + + val result = ASTValidationResult( + isValid = violations.isEmpty(), + violations = violations, + validatedAt = LocalDateTime.now(), + astId = id + ) + + this.isValidated = true + this.validationResult = result + + // ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + addDomainEvent(mapOf( + "eventType" to DomainEvents.AST_VALIDATED, + "aggregateId" to id, + "aggregateType" to DomainEvents.EXPRESSION_AST, + "payload" to mapOf( + "isValid" to result.isValid, + "violations" to result.violations, + "validatedAt" to result.validatedAt.toString() + ) + )) + + return result + } + + /** + * AST๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun optimize(level: OptimizationLevel = OptimizationLevel.FULL): ASTOptimizationResult { + val originalSize = getSize() + val originalDepth = getDepth() + val originalRoot = this.root + + // ์ตœ์ ํ™” ์ˆ˜ํ–‰ + val optimizedRoot = when (level) { + OptimizationLevel.NONE -> this.root + OptimizationLevel.BASIC -> performBasicOptimization(this.root) + OptimizationLevel.FULL -> optimizer.optimize(this.root) + } + + // ๋ฃจํŠธ ์—…๋ฐ์ดํŠธ + this.root = optimizedRoot + this.optimizationLevel = level + this.lastModifiedAt = LocalDateTime.now() + this.isValidated = false + this.validationResult = null + + val optimizedSize = getSize() + val optimizedDepth = getDepth() + + val result = ASTOptimizationResult( + originalSize = originalSize, + optimizedSize = optimizedSize, + originalDepth = originalDepth, + optimizedDepth = optimizedDepth, + level = level, + optimizedAt = LocalDateTime.now(), + astId = id + ) + + // ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + addDomainEvent(mapOf( + "eventType" to DomainEvents.AST_OPTIMIZED, + "aggregateId" to id, + "aggregateType" to DomainEvents.EXPRESSION_AST, + "payload" to mapOf( + "originalRoot" to originalRoot.toString(), + "optimizedRoot" to optimizedRoot.toString(), + "originalSize" to originalSize.value, + "optimizedSize" to optimizedSize.value, + "level" to level.name, + "optimizedAt" to LocalDateTime.now().toString() + ) + )) + + return result + } + + /** + * ๊ธฐ๋ณธ ์ตœ์ ํ™”๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * ๋ชจ๋“  ์ž์‹ ๋…ธ๋“œ๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์ตœ์ ํ™”ํ•œ ํ›„ ํ˜„์žฌ ๋…ธ๋“œ์˜ ์ตœ์ ํ™”๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performBasicOptimization(node: ASTNode): ASTNode { + // ๋จผ์ € ๋ชจ๋“  ์ž์‹ ๋…ธ๋“œ๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ์ตœ์ ํ™” + val optimizedNode = when (node) { + is BinaryOpNode -> BinaryOpNode( + left = performBasicOptimization(node.left), + operator = node.operator, + right = performBasicOptimization(node.right) + ) + is UnaryOpNode -> UnaryOpNode( + operator = node.operator, + operand = performBasicOptimization(node.operand) + ) + is FunctionCallNode -> FunctionCallNode( + name = node.name, + args = node.args.map { performBasicOptimization(it) } + ) + is IfNode -> IfNode( + condition = performBasicOptimization(node.condition), + trueValue = performBasicOptimization(node.trueValue), + falseValue = performBasicOptimization(node.falseValue) + ) + is ArgumentsNode -> ArgumentsNode( + arguments = node.arguments.map { performBasicOptimization(it) } + ) + // ๋ฆฌํ”„ ๋…ธ๋“œ๋“ค์€ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ + is NumberNode, is BooleanNode, is VariableNode -> node + else -> node + } + + // ์ž์‹ ๋…ธ๋“œ ์ตœ์ ํ™” ํ›„ ํ˜„์žฌ ๋…ธ๋“œ์˜ ์ตœ์ ํ™” ์ˆ˜ํ–‰ + return when (optimizedNode) { + is IfNode -> optimizedNode.optimize() + is UnaryOpNode -> optimizedNode.simplify() + is BinaryOpNode -> optimizedNode.simplify() + else -> optimizedNode + } + } + + /** + * ํŠน์ • ๋…ธ๋“œ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + fun findNode(condition: (ASTNode) -> Boolean): ASTNode? { + return traverser.depthFirstSearch(root, condition) + } + + /** + * ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ๋ชจ๋“  ๋…ธ๋“œ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + fun findAllNodes(condition: (ASTNode) -> Boolean): List { + return traverser.findAll(root, condition) + } + + /** + * ํŠน์ • ํƒ€์ž…์˜ ๋…ธ๋“œ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + fun findNodesByType(nodeClass: Class): List { + return traverser.findByType(root, nodeClass) + } + + /** + * ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + */ + fun accept(visitor: ASTVisitor) { + traverser.preOrderTraversal(root, visitor) + } + + /** + * ํŠธ๋ฆฌ ํ†ต๊ณ„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun getStatistics(): TreeStatistics { + val stats = traverser.calculateStatistics(root) + return TreeStatistics( + nodeCount = stats.nodeCount, + leafCount = stats.leafCount, + maxDepth = stats.maxDepth, + averageDepth = stats.averageDepth, + nodeTypeCounts = stats.nodeTypeCounts, + variables = getVariables(), + astId = id, + calculatedAt = LocalDateTime.now() + ) + } + + /** + * ์„œ๋ธŒํŠธ๋ฆฌ๋ฅผ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค. + */ + fun replaceSubtree(target: ASTNode, replacement: ASTNode) { + if (!validitySpec.isSatisfiedBy(replacement)) { + val reason = validitySpec.getWhyNotSatisfied(replacement) + throw ASTException.invalidReplacementNode(reason, replacement.toString()) + } + + val newRoot = replaceSubtreeHelper(root, target, replacement) + + if (newRoot != root) { + setRoot(newRoot) + addDomainEvent(mapOf( + "eventType" to DomainEvents.SUBTREE_REPLACED, + "aggregateId" to id, + "aggregateType" to DomainEvents.EXPRESSION_AST, + "payload" to mapOf( + "target" to target.toString(), + "replacement" to replacement.toString(), + "replacedAt" to LocalDateTime.now().toString() + ) + )) + } + } + + /** + * ์„œ๋ธŒํŠธ๋ฆฌ ๊ต์ฒด ํ—ฌํผ ํ•จ์ˆ˜ + */ + private fun replaceSubtreeHelper(current: ASTNode, target: ASTNode, replacement: ASTNode): ASTNode { + if (current == target) { + return replacement + } + + return when (current) { + is BinaryOpNode -> { + val newLeft = replaceSubtreeHelper(current.left, target, replacement) + val newRight = replaceSubtreeHelper(current.right, target, replacement) + if (newLeft != current.left || newRight != current.right) { + factory.createBinaryOp(newLeft, current.operator, newRight) + } else { + current + } + } + is UnaryOpNode -> { + val newOperand = replaceSubtreeHelper(current.operand, target, replacement) + if (newOperand != current.operand) { + factory.createUnaryOp(current.operator, newOperand) + } else { + current + } + } + is FunctionCallNode -> { + val newArgs = current.args.map { replaceSubtreeHelper(it, target, replacement) } + if (newArgs != current.args) { + factory.createFunctionCall(current.name, newArgs) + } else { + current + } + } + is IfNode -> { + val newCondition = replaceSubtreeHelper(current.condition, target, replacement) + val newTrueValue = replaceSubtreeHelper(current.trueValue, target, replacement) + val newFalseValue = replaceSubtreeHelper(current.falseValue, target, replacement) + if (newCondition != current.condition || newTrueValue != current.trueValue || newFalseValue != current.falseValue) { + factory.createIf(newCondition, newTrueValue, newFalseValue) + } else { + current + } + } + is ArgumentsNode -> { + val newArgs = current.arguments.map { replaceSubtreeHelper(it, target, replacement) } + if (newArgs != current.arguments) { + factory.createArguments(newArgs) + } else { + current + } + } + else -> current + } + } + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun addDomainEvent(event: Any) { + domainEvents.add(event) + } + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getDomainEvents(): List = domainEvents.toList() + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋“ค์„ ํด๋ฆฌ์–ดํ•ฉ๋‹ˆ๋‹ค. + */ + fun clearDomainEvents() { + domainEvents.clear() + } + + /** + * ๋ณต์‚ฌ๋ณธ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun copy(): ExpressionAST { + return ExpressionAST( + id = UUID.randomUUID().toString(), + root = root, + createdAt = LocalDateTime.now(), + lastModifiedAt = LocalDateTime.now(), + optimizationLevel = OptimizationLevel.NONE, + isValidated = false, + validationResult = null + ) + } + + /** + * ๋ฌธ์ž์—ด ํ‘œํ˜„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun toString(): String { + return "ExpressionAST(id='$id', size=${getSize().value}, depth=${getDepth().value})" + } + + /** + * ๊ฐ™์€ ๊ฐ์ฒด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ExpressionAST) return false + return id == other.id + } + + /** + * ํ•ด์‹œ ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun hashCode(): Int { + return id.hashCode() + } + + companion object { + /** + * ์ƒˆ๋กœ์šด ExpressionAST๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun create(root: ASTNode): ExpressionAST { + val ast = ExpressionAST( + id = UUID.randomUUID().toString(), + root = root, + createdAt = LocalDateTime.now(), + lastModifiedAt = LocalDateTime.now(), + optimizationLevel = OptimizationLevel.NONE, + isValidated = false, + validationResult = null + ) + + // ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + ast.addDomainEvent(mapOf( + "eventType" to DomainEvents.AST_CREATED, + "aggregateId" to ast.id, + "aggregateType" to DomainEvents.EXPRESSION_AST, + "payload" to mapOf( + "root" to root.toString(), + "createdAt" to LocalDateTime.now().toString() + ) + )) + + return ast + } + + /** + * ID๋กœ ExpressionAST๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createWithId(id: String, root: ASTNode): ExpressionAST { + val ast = ExpressionAST( + id = id, + root = root, + createdAt = LocalDateTime.now(), + lastModifiedAt = LocalDateTime.now(), + optimizationLevel = OptimizationLevel.NONE, + isValidated = false, + validationResult = null + ) + + // ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ + ast.addDomainEvent(mapOf( + "eventType" to DomainEvents.AST_CREATED, + "aggregateId" to id, + "aggregateType" to DomainEvents.EXPRESSION_AST, + "payload" to mapOf( + "root" to root.toString(), + "createdAt" to LocalDateTime.now().toString() + ) + )) + + return ast + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ASTNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ASTNode.kt new file mode 100644 index 00000000..934bb275 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ASTNode.kt @@ -0,0 +1,197 @@ +package hs.kr.entrydsm.domain.ast.entities + +import hs.kr.entrydsm.global.interfaces.EntityMarker +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ(AST)์˜ ๋ชจ๋“  ๋…ธ๋“œ์— ๋Œ€ํ•œ ๊ธฐ๋ณธ sealed ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์˜ ๋ชจ๋“  ๊ตฌ๋ฌธ ์š”์†Œ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” AST ๋…ธ๋“œ๋“ค์˜ ๊ธฐ๋ณธ ์ธํ„ฐํŽ˜์ด์Šค๋กœ, + * Visitor ํŒจํ„ด์„ ์ง€์›ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ์—ฐ์‚ฐ(ํ‰๊ฐ€, ์ถœ๋ ฅ, ๋ณ€ํ™˜ ๋“ฑ)์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * ๋ชจ๋“  AST ๋…ธ๋“œ๋Š” ์ด ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„์•ผ ํ•˜๋ฉฐ, ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast") +sealed class ASTNode : EntityMarker { + + private val id: String = java.util.UUID.randomUUID().toString() + + override fun getDomainContext(): String = AST + + override fun getIdentifier(): String = id + + /** + * ์ด AST ๋…ธ๋“œ์— ํฌํ•จ๋œ ๋ชจ๋“  ๋ณ€์ˆ˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * ํ‘œํ˜„์‹์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ณ€์ˆ˜๋“ค์„ ์ถ”์ถœํ•˜์—ฌ ๋ณ€์ˆ˜ ์˜์กด์„ฑ ๋ถ„์„์ด๋‚˜ + * ๋ณ€์ˆ˜ ๊ฐ’ ๊ฒ€์ฆ์— ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @return ๋ณ€์ˆ˜ ์ด๋ฆ„์˜ ์ง‘ํ•ฉ + */ + abstract fun getVariables(): Set + + /** + * AST ๋…ธ๋“œ์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋…ธ๋“œ ํƒ€์ž… ๋ฌธ์ž์—ด + */ + fun getNodeType(): String = this::class.simpleName ?: UNKNOWN_NODE + + /** + * AST ๋…ธ๋“œ๊ฐ€ ๋ฆฌํ„ฐ๋Ÿด ๊ฐ’์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฆฌํ„ฐ๋Ÿด์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + open fun isLiteral(): Boolean = false + + /** + * AST ๋…ธ๋“œ๊ฐ€ ๋ฆฌํ”„ ๋…ธ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * ๋ฆฌํ”„ ๋…ธ๋“œ๋Š” ์ž์‹ ๋…ธ๋“œ๊ฐ€ ์—†๋Š” ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. + * + * @return ๋ฆฌํ”„ ๋…ธ๋“œ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + open fun isLeaf(): Boolean = getChildren().isEmpty() + + /** + * ์ด ๋…ธ๋“œ์˜ ๋ชจ๋“  ์ž์‹ ๋…ธ๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž์‹ ๋…ธ๋“œ๋“ค์˜ ๋ฆฌ์ŠคํŠธ + */ + abstract fun getChildren(): List + + /** + * AST ๋…ธ๋“œ๊ฐ€ ๋ณ€์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + open fun isVariable(): Boolean = false + + /** + * AST ๋…ธ๋“œ๊ฐ€ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + open fun isOperator(): Boolean = false + + /** + * AST ๋…ธ๋“œ๊ฐ€ ํ•จ์ˆ˜ ํ˜ธ์ถœ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + open fun isFunctionCall(): Boolean = false + + /** + * AST ๋…ธ๋“œ๊ฐ€ ์กฐ๊ฑด๋ฌธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์กฐ๊ฑด๋ฌธ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + open fun isConditional(): Boolean = false + + /** + * AST ๋…ธ๋“œ์˜ ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋…ธ๋“œ์˜ ์ตœ๋Œ€ ๊นŠ์ด + */ + abstract fun getDepth(): Int + + /** + * AST ๋…ธ๋“œ์˜ ์ด ๋…ธ๋“œ ๊ฐœ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•˜์œ„ ๋…ธ๋“œ๋ฅผ ํฌํ•จํ•œ ์ด ๋…ธ๋“œ ๊ฐœ์ˆ˜ + */ + abstract fun getNodeCount(): Int + + /** + * AST ๋…ธ๋“œ์˜ ํฌ๊ธฐ(์ด ๋…ธ๋“œ ๊ฐœ์ˆ˜)๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * getNodeCount()์™€ ๋™์ผํ•˜์ง€๋งŒ ๋‹ค๋ฅธ ์ปจํ…์ŠคํŠธ์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @return ํ•˜์œ„ ๋…ธ๋“œ๋ฅผ ํฌํ•จํ•œ ์ด ๋…ธ๋“œ ๊ฐœ์ˆ˜ + */ + fun getSize(): Int = getNodeCount() + + /** + * AST ๋…ธ๋“œ๋ฅผ ๋ณต์ œํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณต์ œ๋œ AST ๋…ธ๋“œ + */ + abstract fun copy(): ASTNode + + /** + * AST ๋…ธ๋“œ์˜ ๊ตฌ์กฐ๋ฅผ ํŠธ๋ฆฌ ํ˜•ํƒœ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param indent ๋“ค์—ฌ์“ฐ๊ธฐ ๋ ˆ๋ฒจ + * @return ํŠธ๋ฆฌ ๊ตฌ์กฐ ๋ฌธ์ž์—ด + */ + open fun toTreeString(indent: Int = 0): String { + val spaces = " ".repeat(indent) + return "$spaces${getNodeType()}: $this" + } + + /** + * AST ๋…ธ๋“œ๋ฅผ ๊ด„ํ˜ธ ์—†์ด ๊ฐ„๋‹จํ•œ ํ˜•ํƒœ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ„๋‹จํ•œ ๋ฌธ์ž์—ด ํ‘œํ˜„ + */ + abstract fun toSimpleString(): String + + /** + * Visitor ํŒจํ„ด์˜ accept ๋ฉ”์„œ๋“œ๋ฅผ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param visitor ๋ฐฉ๋ฌธ์ž ๊ฐ์ฒด + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + abstract fun accept(visitor: ASTVisitor): T + + /** + * ๋‘ AST ๋…ธ๋“œ๊ฐ€ ๊ตฌ์กฐ์ ์œผ๋กœ ๋™์ผํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  AST ๋…ธ๋“œ + * @return ๊ตฌ์กฐ์ ์œผ๋กœ ๋™์ผํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + abstract fun isStructurallyEqual(other: ASTNode): Boolean + + /** + * AST ๋…ธ๋“œ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * ๊ธฐ๋ณธ ๊ตฌํ˜„์—์„œ๋Š” true๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋ฉฐ, ํ•„์š”์— ๋”ฐ๋ผ ํ•˜์œ„ ํด๋ž˜์Šค์—์„œ ์žฌ์ •์˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + open fun validate(): Boolean = true + + /** + * ์กฐ๊ฑด๋ฌธ์˜ ์ค‘์ฒฉ ๊นŠ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * ๊ธฐ๋ณธ๊ฐ’์€ 0์ด๋ฉฐ, ์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ์—์„œ ์žฌ์ •์˜๋ฉ๋‹ˆ๋‹ค. + * + * @return ์ค‘์ฒฉ ๊นŠ์ด + */ + open fun getNestingDepth(): Int = 0 + + companion object { + /** + * AST ๋…ธ๋“œ ํƒ€์ž…์„ ํ™•์ธํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + * + * @param node ํ™•์ธํ•  ๋…ธ๋“œ + * @return ๋…ธ๋“œ ํƒ€์ž… ์ •๋ณด + */ + fun getNodeInfo(node: ASTNode): Map = mapOf( + "type" to node.getNodeType(), + "isLiteral" to node.isLiteral(), + "isVariable" to node.isVariable(), + "isOperator" to node.isOperator(), + "isFunctionCall" to node.isFunctionCall(), + "isConditional" to node.isConditional(), + "depth" to node.getDepth(), + "nodeCount" to node.getNodeCount(), + "variables" to node.getVariables() + ) + + const val AST = "ast" + const val UNKNOWN_NODE = "UnknownNode" + } +} + diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ArgumentsNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ArgumentsNode.kt new file mode 100644 index 00000000..6fbff0a1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/ArgumentsNode.kt @@ -0,0 +1,351 @@ +package hs.kr.entrydsm.domain.ast.entities + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ํ•จ์ˆ˜ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ๋‚˜ํƒ€๋‚ด๋Š” AST ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ํ•จ์ˆ˜ ์ธ์ˆ˜๋“ค์„ ๊ทธ๋ฃนํ™”ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋ฉฐ, + * ์ตœ์ข…์ ์œผ๋กœ๋Š” FunctionCallNode์— ํ†ตํ•ฉ๋ฉ๋‹ˆ๋‹ค. + * + * @property arguments ์ธ์ˆ˜ ๋ชฉ๋ก + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast") +data class ArgumentsNode( + val arguments: List +) : ASTNode() { + + init { + if (arguments.size > MAX_ARGUMENTS) { + throw ASTException.argumentCountExceeded() + } + } + + override fun accept(visitor: ASTVisitor): T = visitor.visitArguments(this) + + override fun getVariables(): Set = arguments.flatMap { it.getVariables() }.toSet() + + override fun getChildren(): List = arguments + + override fun getDepth(): Int = 1 + (arguments.maxOfOrNull { it.getDepth() } ?: 0) + + override fun getNodeCount(): Int = 1 + arguments.sumOf { it.getNodeCount() } + + override fun copy(): ASTNode = ArgumentsNode(arguments.map { it.copy() }) + + override fun toSimpleString(): String = arguments.joinToString(", ") { it.toSimpleString() } + + override fun isStructurallyEqual(other: ASTNode): Boolean { + return other is ArgumentsNode && + this.arguments.size == other.arguments.size && + this.arguments.zip(other.arguments).all { (thisArg, otherArg) -> + thisArg.isStructurallyEqual(otherArg) + } + } + + /** + * ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + */ + fun getArgumentCount(): Int = arguments.size + + /** + * ์ธ์ˆ˜๊ฐ€ ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด true + */ + fun isEmpty(): Boolean = arguments.isEmpty() + + /** + * ์ธ์ˆ˜๊ฐ€ ํ•˜๋‚˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜๊ฐ€ ํ•˜๋‚˜์ด๋ฉด true + */ + fun isSingle(): Boolean = arguments.size == 1 + + /** + * ์ธ์ˆ˜๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜๊ฐ€ ์—ฌ๋Ÿฌ ๊ฐœ์ด๋ฉด true + */ + fun isMultiple(): Boolean = arguments.size > 1 + + /** + * ํŠน์ • ์ธ๋ฑ์Šค์˜ ์ธ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ์ธ์ˆ˜ ์ธ๋ฑ์Šค + * @return ํ•ด๋‹น ์ธ๋ฑ์Šค์˜ ์ธ์ˆ˜, ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚˜๋ฉด null + */ + fun getArgument(index: Int): ASTNode? { + return if (index in 0 until arguments.size) arguments[index] else null + } + + /** + * ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜, ์—†์œผ๋ฉด null + */ + fun getFirst(): ASTNode? = arguments.firstOrNull() + + /** + * ๋งˆ์ง€๋ง‰ ์ธ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋งˆ์ง€๋ง‰ ์ธ์ˆ˜, ์—†์œผ๋ฉด null + */ + fun getLast(): ASTNode? = arguments.lastOrNull() + + /** + * ์ธ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param argument ์ถ”๊ฐ€ํ•  ์ธ์ˆ˜ + * @return ์ƒˆ๋กœ์šด ArgumentsNode + */ + fun addArgument(argument: ASTNode): ArgumentsNode { + return ArgumentsNode(arguments + argument) + } + + /** + * ํŠน์ • ์ธ๋ฑ์Šค์— ์ธ์ˆ˜๋ฅผ ์‚ฝ์ž…ํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ์‚ฝ์ž…ํ•  ์ธ๋ฑ์Šค + * @param argument ์‚ฝ์ž…ํ•  ์ธ์ˆ˜ + * @return ์ƒˆ๋กœ์šด ArgumentsNode + */ + fun insertArgument(index: Int, argument: ASTNode): ArgumentsNode { + if (index !in 0..arguments.size) { + throw ASTException.indexOutOfRange() + } + val newArguments = arguments.toMutableList() + newArguments.add(index, argument) + return ArgumentsNode(newArguments) + } + + /** + * ํŠน์ • ์ธ๋ฑ์Šค์˜ ์ธ์ˆ˜๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ์ œ๊ฑฐํ•  ์ธ์ˆ˜์˜ ์ธ๋ฑ์Šค + * @return ์ƒˆ๋กœ์šด ArgumentsNode + */ + fun removeArgument(index: Int): ArgumentsNode { + if (index !in 0..arguments.size) { + throw ASTException.indexOutOfRange() + } + return ArgumentsNode(arguments.filterIndexed { i, _ -> i != index }) + } + + /** + * ํŠน์ • ์ธ๋ฑ์Šค์˜ ์ธ์ˆ˜๋ฅผ ๊ต์ฒดํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ๊ต์ฒดํ•  ์ธ์ˆ˜์˜ ์ธ๋ฑ์Šค + * @param newArgument ์ƒˆ๋กœ์šด ์ธ์ˆ˜ + * @return ์ƒˆ๋กœ์šด ArgumentsNode + */ + fun replaceArgument(index: Int, newArgument: ASTNode): ArgumentsNode { + if (index !in 0..arguments.size) { + throw ASTException.indexOutOfRange() + } + val newArguments = arguments.toMutableList() + newArguments[index] = newArgument + return ArgumentsNode(newArguments) + } + + /** + * ๋ชจ๋“  ์ธ์ˆ˜๊ฐ€ ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ชจ๋“  ์ธ์ˆ˜๊ฐ€ ๋ฆฌํ„ฐ๋Ÿด์ด๋ฉด true + */ + fun areAllLiterals(): Boolean = arguments.all { it.isLiteral() } + + /** + * ๋ชจ๋“  ์ธ์ˆ˜๊ฐ€ ์ˆซ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ชจ๋“  ์ธ์ˆ˜๊ฐ€ ์ˆซ์ž์ด๋ฉด true + */ + fun areAllNumbers(): Boolean = arguments.all { it is NumberNode } + + /** + * ๋ชจ๋“  ์ธ์ˆ˜๊ฐ€ ๋ถˆ๋ฆฌ์–ธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ชจ๋“  ์ธ์ˆ˜๊ฐ€ ๋ถˆ๋ฆฌ์–ธ์ด๋ฉด true + */ + fun areAllBooleans(): Boolean = arguments.all { it is BooleanNode } + + /** + * ๋ชจ๋“  ์ธ์ˆ˜๊ฐ€ ๋ณ€์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ชจ๋“  ์ธ์ˆ˜๊ฐ€ ๋ณ€์ˆ˜์ด๋ฉด true + */ + fun areAllVariables(): Boolean = arguments.all { it is VariableNode } + + /** + * ํŠน์ • ํƒ€์ž…์˜ ์ธ์ˆ˜๋งŒ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeType ํ™•์ธํ•  ๋…ธ๋“œ ํƒ€์ž… + * @return ํ•ด๋‹น ํƒ€์ž…์˜ ์ธ์ˆ˜๋งŒ ์žˆ์œผ๋ฉด true + */ + fun hasOnlyType(nodeType: String): Boolean = arguments.all { it.getNodeType() == nodeType } + + /** + * ํŠน์ • ํƒ€์ž…์˜ ์ธ์ˆ˜๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeType ํ™•์ธํ•  ๋…ธ๋“œ ํƒ€์ž… + * @return ํ•ด๋‹น ํƒ€์ž…์˜ ์ธ์ˆ˜๊ฐ€ ์žˆ์œผ๋ฉด true + */ + fun hasType(nodeType: String): Boolean = arguments.any { it.getNodeType() == nodeType } + + /** + * ์ธ์ˆ˜ ํƒ€์ž… ๋ถ„ํฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜ ํƒ€์ž…๋ณ„ ๊ฐœ์ˆ˜ + */ + fun getTypeDistribution(): Map { + return arguments.groupingBy { it.getNodeType() }.eachCount() + } + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก์„ ๋ฆฌ์ŠคํŠธ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜ ๋ฆฌ์ŠคํŠธ + */ + fun toList(): List = arguments.toList() + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก์„ ์—ญ์ˆœ์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ญ์ˆœ ArgumentsNode + */ + fun reverse(): ArgumentsNode = ArgumentsNode(arguments.reversed()) + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก์„ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param comparator ๋น„๊ต ํ•จ์ˆ˜ + * @return ์ •๋ ฌ๋œ ArgumentsNode + */ + fun sort(comparator: Comparator): ArgumentsNode { + return ArgumentsNode(arguments.sortedWith(comparator)) + } + + /** + * ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ์ธ์ˆ˜๋“ค์„ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. + * + * @param predicate ์กฐ๊ฑด ํ•จ์ˆ˜ + * @return ํ•„ํ„ฐ๋ง๋œ ArgumentsNode + */ + fun filter(predicate: (ASTNode) -> Boolean): ArgumentsNode { + return ArgumentsNode(arguments.filter(predicate)) + } + + /** + * ์ธ์ˆ˜๋“ค์„ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param transform ๋ณ€ํ™˜ ํ•จ์ˆ˜ + * @return ๋ณ€ํ™˜๋œ ArgumentsNode + */ + fun map(transform: (ASTNode) -> ASTNode): ArgumentsNode { + return ArgumentsNode(arguments.map(transform)) + } + + /** + * ์ธ์ˆ˜ ๋…ธ๋“œ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜ ์ •๋ณด ๋งต + */ + fun getArgumentsInfo(): Map { + return mapOf( + "count" to getArgumentCount(), + "isEmpty" to isEmpty(), + "isSingle" to isSingle(), + "isMultiple" to isMultiple(), + "areAllLiterals" to areAllLiterals(), + "areAllNumbers" to areAllNumbers(), + "areAllBooleans" to areAllBooleans(), + "areAllVariables" to areAllVariables(), + "typeDistribution" to getTypeDistribution(), + "argumentTypes" to arguments.map { it.getNodeType() }, + "variables" to getVariables(), + "totalSize" to getNodeCount(), + "maxDepth" to (arguments.maxOfOrNull { it.getDepth() } ?: 0) + ) + } + + override fun toString(): String = arguments.joinToString(", ", "[", "]") + + companion object { + private const val MAX_ARGUMENTS = 100 + + /** + * ๋นˆ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋นˆ ArgumentsNode + */ + fun empty(): ArgumentsNode = ArgumentsNode(emptyList()) + + /** + * ๋‹จ์ผ ์ธ์ˆ˜๋กœ ArgumentsNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param argument ์ธ์ˆ˜ + * @return ArgumentsNode ์ธ์Šคํ„ด์Šค + */ + fun single(argument: ASTNode): ArgumentsNode = ArgumentsNode(listOf(argument)) + + /** + * ์—ฌ๋Ÿฌ ์ธ์ˆ˜๋กœ ArgumentsNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param arguments ์ธ์ˆ˜๋“ค + * @return ArgumentsNode ์ธ์Šคํ„ด์Šค + */ + fun of(vararg arguments: ASTNode): ArgumentsNode = ArgumentsNode(arguments.toList()) + + /** + * ๋ฆฌ์ŠคํŠธ๋กœ๋ถ€ํ„ฐ ArgumentsNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param arguments ์ธ์ˆ˜ ๋ฆฌ์ŠคํŠธ + * @return ArgumentsNode ์ธ์Šคํ„ด์Šค + */ + fun from(arguments: List): ArgumentsNode = ArgumentsNode(arguments) + + /** + * ์ธ์ˆ˜ ๋…ธ๋“œ ๋ชฉ๋ก์˜ ํ†ต๊ณ„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodes ArgumentsNode ๋ชฉ๋ก + * @return ํ†ต๊ณ„ ์ •๋ณด + */ + fun calculateStatistics(nodes: List): Map { + if (nodes.isEmpty()) { + return mapOf( + "count" to 0, + "isEmpty" to true + ) + } + + val argumentCounts = nodes.map { it.getArgumentCount() } + val averageArgCount = argumentCounts.average() + val maxArgCount = argumentCounts.maxOrNull() ?: 0 + val minArgCount = argumentCounts.minOrNull() ?: 0 + val emptyCount = nodes.count { it.isEmpty() } + val singleCount = nodes.count { it.isSingle() } + val multipleCount = nodes.count { it.isMultiple() } + + return mapOf( + "count" to nodes.size, + "averageArgCount" to averageArgCount, + "maxArgCount" to maxArgCount, + "minArgCount" to minArgCount, + "emptyCount" to emptyCount, + "singleCount" to singleCount, + "multipleCount" to multipleCount, + "isEmpty" to false, + "argCountRange" to (maxArgCount - minArgCount) + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BinaryOpNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BinaryOpNode.kt new file mode 100644 index 00000000..bffb6ea1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BinaryOpNode.kt @@ -0,0 +1,443 @@ +package hs.kr.entrydsm.domain.ast.entities + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.entities.Entity +import kotlin.math.pow + +/** + * ์ดํ•ญ ์—ฐ์‚ฐ(์˜ˆ: ๋ง์…ˆ, ๋บ„์…ˆ, ๋น„๊ต)์„ ๋‚˜ํƒ€๋‚ด๋Š” AST ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“  ์ดํ•ญ ์—ฐ์‚ฐ์ž๋ฅผ ํ‘œํ˜„ํ•˜๋ฉฐ, ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž, + * ์—ฐ์‚ฐ์ž, ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์‚ฐ์ˆ  ์—ฐ์‚ฐ, ๋น„๊ต ์—ฐ์‚ฐ, ๋…ผ๋ฆฌ ์—ฐ์‚ฐ ๋“ฑ์„ + * ๋ชจ๋‘ ์ง€์›ํ•˜๋ฉฐ, ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋˜์–ด ์•ˆ์ „ํ•œ ์—ฐ์‚ฐ ํŠธ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @property left ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž AST ๋…ธ๋“œ + * @property operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด (์˜ˆ: "+", "-", "==") + * @property right ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž AST ๋…ธ๋“œ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast") +data class BinaryOpNode( + val left: ASTNode, + val operator: String, + val right: ASTNode +) : ASTNode() { + + init { + if (operator.isBlank()) { + throw ASTException.operatorEmpty() + } + + if (!isSupportedOperator(operator)) { + throw ASTException.unsupportedOperator() + } + } + + override fun getVariables(): Set = left.getVariables() + right.getVariables() + + override fun getChildren(): List = listOf(left, right) + + override fun isOperator(): Boolean = true + + override fun getDepth(): Int = maxOf(left.getDepth(), right.getDepth()) + 1 + + override fun getNodeCount(): Int = left.getNodeCount() + right.getNodeCount() + 1 + + override fun copy(): BinaryOpNode = BinaryOpNode(left.copy(), operator, right.copy()) + + override fun toSimpleString(): String = "($left $operator $right)" + + override fun accept(visitor: ASTVisitor): T = visitor.visitBinaryOp(this) + + override fun isStructurallyEqual(other: ASTNode): Boolean { + if (other !is BinaryOpNode || this.operator != other.operator) return false + + // ๊ธฐ๋ณธ์ ์ธ ์ˆœ์„œ๋Œ€๋กœ ๋น„๊ต + val standardMatch = this.left.isStructurallyEqual(other.left) && + this.right.isStructurallyEqual(other.right) + + // ๊ตํ™˜๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜๋Š” ์—ฐ์‚ฐ์ž์˜ ๊ฒฝ์šฐ ์ˆœ์„œ๋ฅผ ๋ฐ”๊ฟ”์„œ๋„ ๋น„๊ต + val commutativeMatch = if (isCommutativeOperator(operator)) { + this.left.isStructurallyEqual(other.right) && + this.right.isStructurallyEqual(other.left) + } else false + + return standardMatch || commutativeMatch + } + + /** + * ๊ตํ™˜๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜๋Š” ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param op ํ™•์ธํ•  ์—ฐ์‚ฐ์ž + * @return ๊ตํ™˜๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜๋ฉด true + */ + private fun isCommutativeOperator(op: String): Boolean = when (op) { + "+", "*", "==", "!=", "&&", "||" -> true + else -> false + } + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ์ง€์›๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param op ํ™•์ธํ•  ์—ฐ์‚ฐ์ž + * @return ์ง€์›๋˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + private fun isSupportedOperator(op: String): Boolean = op in SUPPORTED_OPERATORS + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isArithmeticOperator(): Boolean = operator in ARITHMETIC_OPERATORS + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ๋น„๊ต ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋น„๊ต ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isComparisonOperator(): Boolean = operator in COMPARISON_OPERATORS + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isLogicalOperator(): Boolean = operator in LOGICAL_OPERATORS + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ๊ตํ™˜๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ตํ™˜๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isCommutative(): Boolean = operator in COMMUTATIVE_OPERATORS + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ๊ฒฐํ•ฉ๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฒฐํ•ฉ๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAssociative(): Boolean = operator in ASSOCIATIVE_OPERATORS + + /** + * ์—ฐ์‚ฐ์ž์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ๋จผ์ € ๊ณ„์‚ฐ) + */ + fun getPrecedence(): Int = OPERATOR_PRECEDENCE[operator] ?: 0 + + /** + * ์—ฐ์‚ฐ์ž์˜ ๊ฒฐํ•ฉ์„ฑ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฒฐํ•ฉ์„ฑ (LEFT ๋˜๋Š” RIGHT) + */ + fun getAssociativity(): Associativity = OPERATOR_ASSOCIATIVITY[operator] ?: Associativity.LEFT + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ์ขŒ๊ฒฐํ•ฉ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ขŒ๊ฒฐํ•ฉ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isLeftAssociative(): Boolean = getAssociativity() == Associativity.LEFT + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ์šฐ๊ฒฐํ•ฉ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ๊ฒฐํ•ฉ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isRightAssociative(): Boolean = getAssociativity() == Associativity.RIGHT + + /** + * ์—ฐ์‚ฐ์ž์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฐ์‚ฐ์ž ํƒ€์ž… + */ + fun getOperatorType(): OperatorType = when { + isArithmeticOperator() -> OperatorType.ARITHMETIC + isComparisonOperator() -> OperatorType.COMPARISON + isLogicalOperator() -> OperatorType.LOGICAL + else -> OperatorType.UNKNOWN + } + + /** + * ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž๋ฅผ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด BinaryOpNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newLeft ์ƒˆ๋กœ์šด ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @return ์ƒˆ๋กœ์šด BinaryOpNode + */ + fun withLeft(newLeft: ASTNode): BinaryOpNode = BinaryOpNode(newLeft, operator, right) + + /** + * ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž๋ฅผ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด BinaryOpNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newRight ์ƒˆ๋กœ์šด ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @return ์ƒˆ๋กœ์šด BinaryOpNode + */ + fun withRight(newRight: ASTNode): BinaryOpNode = BinaryOpNode(left, operator, newRight) + + /** + * ์—ฐ์‚ฐ์ž๋ฅผ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด BinaryOpNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newOperator ์ƒˆ๋กœ์šด ์—ฐ์‚ฐ์ž + * @return ์ƒˆ๋กœ์šด BinaryOpNode + */ + fun withOperator(newOperator: String): BinaryOpNode = BinaryOpNode(left, newOperator, right) + + /** + * ๊ตํ™˜๋ฒ•์น™์„ ์ ์šฉํ•œ ์ƒˆ๋กœ์šด BinaryOpNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ๊ตํ™˜๋œ ์ƒˆ๋กœ์šด BinaryOpNode + * @throws IllegalStateException ๊ตํ™˜๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž์ธ ๊ฒฝ์šฐ + */ + fun commute(): BinaryOpNode { + if (!isCommutative()) { + throw ASTException.operatorNotCommutative(operator) + } + return BinaryOpNode(right, operator, left) + } + + + /** + * BinaryOpNode๋ฅผ ๋‹จ์ˆœํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * ๋‹ค์Œ๊ณผ ๊ฐ™์€ ์ตœ์ ํ™”๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค: + * - ์‚ฐ์ˆ  ์—ฐ์‚ฐ: 0๊ณผ์˜ ๋ง์…ˆ/๋บ„์…ˆ, 1๊ณผ์˜ ๊ณฑ์…ˆ/๋‚˜๋ˆ—์…ˆ, 0๊ณผ์˜ ๊ณฑ์…ˆ + * - ๋…ผ๋ฆฌ ์—ฐ์‚ฐ: true/false์™€์˜ ๋…ผ๋ฆฌ ์—ฐ์‚ฐ + * - ์ƒ์ˆ˜ ๊ณ„์‚ฐ: ๋‘ ์ƒ์ˆ˜์˜ ์—ฐ์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ + * - ๋™์ผํ•œ ํ”ผ์—ฐ์‚ฐ์ž์˜ ์—ฐ์‚ฐ ์ฒ˜๋ฆฌ + * + * @return ๋‹จ์ˆœํ™”๋œ AST ๋…ธ๋“œ + */ + fun simplify(): ASTNode { + // ์–‘์ชฝ ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ NumberNode์ธ ๊ฒฝ์šฐ ์ƒ์ˆ˜ ๊ณ„์‚ฐ + if (left is NumberNode && right is NumberNode) { + return when (operator) { + "+" -> NumberNode(left.value + right.value) + "-" -> NumberNode(left.value - right.value) + "*" -> NumberNode(left.value * right.value) + "/" -> { + if (right.isZero()) return this // 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ๋Š” ๋‹จ์ˆœํ™”ํ•˜์ง€ ์•Š์Œ + NumberNode(left.value / right.value) + } + "%" -> { + if (right.isZero()) return this + NumberNode(left.value % right.value) + } + "^" -> NumberNode(left.value.pow(right.value)) + "==" -> NumberNode(if (left.value == right.value) 1.0 else 0.0) + "!=" -> NumberNode(if (left.value != right.value) 1.0 else 0.0) + "<" -> NumberNode(if (left.value < right.value) 1.0 else 0.0) + "<=" -> NumberNode(if (left.value <= right.value) 1.0 else 0.0) + ">" -> NumberNode(if (left.value > right.value) 1.0 else 0.0) + ">=" -> NumberNode(if (left.value >= right.value) 1.0 else 0.0) + "&&" -> NumberNode(if (!left.isZero() && !right.isZero()) 1.0 else 0.0) + "||" -> NumberNode(if (!left.isZero() || !right.isZero()) 1.0 else 0.0) + else -> this + } + } + + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ ์ตœ์ ํ™” + if (left is NumberNode || right is NumberNode) { + when (operator) { + "+" -> { + if (left is NumberNode && left.isZero()) return right + if (right is NumberNode && right.isZero()) return left + } + "-" -> { + if (right is NumberNode && right.isZero()) return left + if (left is NumberNode && left.isZero()) return UnaryOpNode("-", right) + } + "*" -> { + if ((left is NumberNode && left.isZero()) || (right is NumberNode && right.isZero())) { + return NumberNode.ZERO + } + if (left is NumberNode && left.value == 1.0) return right + if (right is NumberNode && right.value == 1.0) return left + if (left is NumberNode && left.value == -1.0) return UnaryOpNode("-", right) + if (right is NumberNode && right.value == -1.0) return UnaryOpNode("-", left) + } + "/" -> { + if (left is NumberNode && left.isZero()) return NumberNode.ZERO + if (right is NumberNode && right.value == 1.0) return left + if (right is NumberNode && right.value == -1.0) return UnaryOpNode("-", left) + } + "^" -> { + if (right is NumberNode && right.isZero()) return NumberNode.ONE + if (right is NumberNode && right.value == 1.0) return left + if (left is NumberNode && left.value == 1.0) return NumberNode.ONE + if (left is NumberNode && left.isZero()) return NumberNode.ZERO + } + } + } + + // ๋…ผ๋ฆฌ ์—ฐ์‚ฐ ์ตœ์ ํ™” + when (operator) { + "&&" -> { + if (left is NumberNode && left.isZero()) return NumberNode.ZERO + if (right is NumberNode && right.isZero()) return NumberNode.ZERO + if (left is NumberNode && !left.isZero()) return right + if (right is NumberNode && !right.isZero()) return left + } + "||" -> { + if (left is NumberNode && !left.isZero()) return NumberNode.ONE + if (right is NumberNode && !right.isZero()) return NumberNode.ONE + if (left is NumberNode && left.isZero()) return right + if (right is NumberNode && right.isZero()) return left + } + } + + // ๋™์ผํ•œ ํ”ผ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ + if (left.isStructurallyEqual(right)) { + when (operator) { + "-" -> return NumberNode.ZERO + "/" -> return NumberNode.ONE + "%" -> return NumberNode.ZERO + "==" -> return NumberNode.ONE + "!=" -> return NumberNode.ZERO + "<=" -> return NumberNode.ONE + ">=" -> return NumberNode.ONE + "<" -> return NumberNode.ZERO + ">" -> return NumberNode.ZERO + } + } + + return this + } + + /** + * ๊ด„ํ˜ธ ์—†์ด ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„์— ๋”ฐ๋ผ ๋ฌธ์ž์—ด์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ณ ๋ คํ•œ ๋ฌธ์ž์—ด + */ + fun toStringWithPrecedence(): String { + val leftStr = if (left is BinaryOpNode && left.getPrecedence() < getPrecedence()) { + "(${left.toStringWithPrecedence()})" + } else { + left.toSimpleString() + } + + val rightStr = if (right is BinaryOpNode && + (right.getPrecedence() < getPrecedence() || + (right.getPrecedence() == getPrecedence() && getAssociativity() == Associativity.LEFT))) { + "(${right.toStringWithPrecedence()})" + } else { + right.toSimpleString() + } + + return "$leftStr $operator $rightStr" + } + + override fun toString(): String = toStringWithPrecedence() + + override fun toTreeString(indent: Int): String { + val spaces = " ".repeat(indent) + return buildString { + appendLine("${spaces}BinaryOpNode: $operator") + appendLine("${spaces} left:") + appendLine(left.toTreeString(indent + 2)) + appendLine("${spaces} right:") + append(right.toTreeString(indent + 2)) + } + } + + /** + * ์—ฐ์‚ฐ์ž ๊ฒฐํ•ฉ์„ฑ์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class Associativity { + LEFT, RIGHT + } + + /** + * ์—ฐ์‚ฐ์ž ํƒ€์ž…์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class OperatorType { + ARITHMETIC, COMPARISON, LOGICAL, UNKNOWN + } + + companion object { + /** + * ์ง€์›๋˜๋Š” ๋ชจ๋“  ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val SUPPORTED_OPERATORS = setOf( + "+", "-", "*", "/", "^", "%", + "==", "!=", "<", "<=", ">", ">=", + "&&", "||" + ) + + /** + * ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val ARITHMETIC_OPERATORS = setOf("+", "-", "*", "/", "^", "%") + + /** + * ๋น„๊ต ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val COMPARISON_OPERATORS = setOf("==", "!=", "<", "<=", ">", ">=") + + /** + * ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val LOGICAL_OPERATORS = setOf("&&", "||") + + /** + * ๊ตํ™˜๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜๋Š” ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val COMMUTATIVE_OPERATORS = setOf("+", "*", "==", "!=", "&&", "||") + + /** + * ๊ฒฐํ•ฉ๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜๋Š” ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val ASSOCIATIVE_OPERATORS = setOf("+", "*", "&&", "||") + + /** + * ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๋งต์ž…๋‹ˆ๋‹ค. + */ + private val OPERATOR_PRECEDENCE = mapOf( + "||" to 1, + "&&" to 2, + "==" to 3, "!=" to 3, + "<" to 4, "<=" to 4, ">" to 4, ">=" to 4, + "+" to 5, "-" to 5, + "*" to 6, "/" to 6, "%" to 6, + "^" to 7 + ) + + /** + * ์—ฐ์‚ฐ์ž ๊ฒฐํ•ฉ์„ฑ ๋งต์ž…๋‹ˆ๋‹ค. + */ + private val OPERATOR_ASSOCIATIVITY = mapOf( + "||" to Associativity.LEFT, + "&&" to Associativity.LEFT, + "==" to Associativity.LEFT, "!=" to Associativity.LEFT, + "<" to Associativity.LEFT, "<=" to Associativity.LEFT, ">" to Associativity.LEFT, ">=" to Associativity.LEFT, + "+" to Associativity.LEFT, "-" to Associativity.LEFT, + "*" to Associativity.LEFT, "/" to Associativity.LEFT, "%" to Associativity.LEFT, + "^" to Associativity.RIGHT + ) + + /** + * ์ง€์›๋˜๋Š” ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ์—ฐ์‚ฐ์ž ์ง‘ํ•ฉ + */ + fun getSupportedOperators(): Set = SUPPORTED_OPERATORS.toSet() + + /** + * ํŠน์ • ํƒ€์ž…์˜ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ์—ฐ์‚ฐ์ž ํƒ€์ž… + * @return ํ•ด๋‹น ํƒ€์ž…์˜ ์—ฐ์‚ฐ์ž ์ง‘ํ•ฉ + */ + fun getOperatorsByType(type: OperatorType): Set = when (type) { + OperatorType.ARITHMETIC -> ARITHMETIC_OPERATORS + OperatorType.COMPARISON -> COMPARISON_OPERATORS + OperatorType.LOGICAL -> LOGICAL_OPERATORS + OperatorType.UNKNOWN -> emptySet() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BooleanNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BooleanNode.kt new file mode 100644 index 00000000..8825f35b --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/BooleanNode.kt @@ -0,0 +1,180 @@ +package hs.kr.entrydsm.domain.ast.entities + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ๋ถˆ๋ฆฐ ๋ฆฌํ„ฐ๋Ÿด์„ ๋‚˜ํƒ€๋‚ด๋Š” AST ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ถˆ๋ฆฐ ๊ฐ’(true, false)์„ ํ‘œํ˜„ํ•˜๋ฉฐ, + * ๋…ผ๋ฆฌ ์—ฐ์‚ฐ๊ณผ ์กฐ๊ฑด๋ฌธ์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋˜์–ด + * ์•ˆ์ „ํ•œ ๊ฐ’ ์ „๋‹ฌ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @property value ๋…ธ๋“œ์˜ ๋ถˆ๋ฆฐ ๊ฐ’ (true ๋˜๋Š” false) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast") +data class BooleanNode(val value: Boolean) : ASTNode() { + + override fun getVariables(): Set = emptySet() + + override fun getChildren(): List = emptyList() + + override fun isLiteral(): Boolean = true + + override fun getDepth(): Int = 1 + + override fun getNodeCount(): Int = 1 + + override fun copy(): BooleanNode = this + + override fun toSimpleString(): String = value.toString() + + override fun accept(visitor: ASTVisitor): T = visitor.visitBoolean(this) + + override fun isStructurallyEqual(other: ASTNode): Boolean = + other is BooleanNode && this.value == other.value + + /** + * ๋ถˆ๋ฆฐ ๊ฐ’์ด ์ฐธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฐธ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isTrue(): Boolean = value + + /** + * ๋ถˆ๋ฆฐ ๊ฐ’์ด ๊ฑฐ์ง“์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฑฐ์ง“์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isFalse(): Boolean = !value + + /** + * ๋ถˆ๋ฆฐ ๊ฐ’์„ ๋ฐ˜์ „์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @return ๋ฐ˜์ „๋œ ๊ฐ’์„ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด BooleanNode + */ + fun not(): BooleanNode = BooleanNode(!value) + + /** + * ๋‹ค๋ฅธ BooleanNode์™€ ๋…ผ๋ฆฌ๊ณฑ(AND) ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param other AND ์—ฐ์‚ฐํ•  BooleanNode + * @return AND ์—ฐ์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด BooleanNode + */ + infix fun and(other: BooleanNode): BooleanNode = BooleanNode(value && other.value) + + /** + * ๋‹ค๋ฅธ BooleanNode์™€ ๋…ผ๋ฆฌํ•ฉ(OR) ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param other OR ์—ฐ์‚ฐํ•  BooleanNode + * @return OR ์—ฐ์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด BooleanNode + */ + infix fun or(other: BooleanNode): BooleanNode = BooleanNode(value || other.value) + + /** + * ๋‹ค๋ฅธ BooleanNode์™€ ๋ฐฐํƒ€์  ๋…ผ๋ฆฌํ•ฉ(XOR) ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param other XOR ์—ฐ์‚ฐํ•  BooleanNode + * @return XOR ์—ฐ์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด BooleanNode + */ + infix fun xor(other: BooleanNode): BooleanNode = BooleanNode(value xor other.value) + + /** + * ๋‹ค๋ฅธ BooleanNode์™€ ๋™์น˜์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  BooleanNode + * @return ๊ฐ™์œผ๋ฉด true, ๋‹ค๋ฅด๋ฉด false + */ + fun isEqualTo(other: BooleanNode): Boolean = value == other.value + + /** + * ๋‹ค๋ฅธ BooleanNode์™€ ๋น„๋™์น˜์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  BooleanNode + * @return ๋‹ค๋ฅด๋ฉด true, ๊ฐ™์œผ๋ฉด false + */ + fun isNotEqualTo(other: BooleanNode): Boolean = value != other.value + + /** + * ๋ถˆ๋ฆฐ ๊ฐ’์„ ์ •์ˆ˜๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return true๋ฉด 1, false๋ฉด 0 + */ + fun toInt(): Int = if (value) 1 else 0 + + /** + * ๋ถˆ๋ฆฐ ๊ฐ’์„ Double๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return true๋ฉด 1.0, false๋ฉด 0.0 + */ + fun toDouble(): Double = if (value) 1.0 else 0.0 + + /** + * ๋ถˆ๋ฆฐ ๊ฐ’์„ ์ˆซ์ž ๋…ธ๋“œ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return true๋ฉด NumberNode(1.0), false๋ฉด NumberNode(0.0) + */ + fun toNumberNode(): NumberNode = NumberNode(toDouble()) + + override fun toString(): String = value.toString() + + override fun toTreeString(indent: Int): String { + val spaces = " ".repeat(indent) + return "${spaces}BooleanNode: $value" + } + + companion object { + /** + * TRUE๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” BooleanNode์ž…๋‹ˆ๋‹ค. + */ + val TRUE = BooleanNode(true) + + /** + * FALSE๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” BooleanNode์ž…๋‹ˆ๋‹ค. + */ + val FALSE = BooleanNode(false) + + /** + * Boolean ๊ฐ’์œผ๋กœ BooleanNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value Boolean ๊ฐ’ + * @return BooleanNode ์ธ์Šคํ„ด์Šค + */ + fun of(value: Boolean): BooleanNode = if (value) TRUE else FALSE + + /** + * ๋ฌธ์ž์—ด๋กœ๋ถ€ํ„ฐ BooleanNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ถˆ๋ฆฐ ๋ฌธ์ž์—ด ("true", "false", ๋Œ€์†Œ๋ฌธ์ž ๋ฌด๊ด€) + * @return BooleanNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์ž˜๋ชป๋œ ๋ถˆ๋ฆฐ ๋ฌธ์ž์—ด์ธ ๊ฒฝ์šฐ + */ + fun parse(value: String): BooleanNode = when (value.lowercase()) { + "true" -> TRUE + "false" -> FALSE + else -> throw ASTException.invalidBooleanValue(value) + } + + /** + * ์ˆซ์ž๋กœ๋ถ€ํ„ฐ BooleanNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ˆซ์ž ๊ฐ’ (0์ด๋ฉด false, ๊ทธ ์™ธ๋Š” true) + * @return BooleanNode ์ธ์Šคํ„ด์Šค + */ + fun fromNumber(value: Double): BooleanNode = of(value != 0.0) + + /** + * NumberNode๋กœ๋ถ€ํ„ฐ BooleanNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param numberNode NumberNode (0์ด๋ฉด false, ๊ทธ ์™ธ๋Š” true) + * @return BooleanNode ์ธ์Šคํ„ด์Šค + */ + fun fromNumberNode(numberNode: NumberNode): BooleanNode = of(!numberNode.isZero()) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/FunctionCallNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/FunctionCallNode.kt new file mode 100644 index 00000000..f2f01599 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/FunctionCallNode.kt @@ -0,0 +1,343 @@ +package hs.kr.entrydsm.domain.ast.entities + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ๋‚˜ํƒ€๋‚ด๋Š” AST ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ํ‘œํ˜„ํ•˜๋ฉฐ, ํ•จ์ˆ˜๋ช…๊ณผ ์ธ์ˆ˜ ๋ชฉ๋ก์œผ๋กœ + * ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์ˆ˜ํ•™ ํ•จ์ˆ˜(sin, cos, sqrt ๋“ฑ)์™€ ์‚ฌ์šฉ์ž ์ •์˜ ํ•จ์ˆ˜๋ฅผ ๋ชจ๋‘ ์ง€์›ํ•˜๋ฉฐ, + * ๊ฐ€๋ณ€ ์ธ์ˆ˜์™€ ์„ ํƒ์  ์ธ์ˆ˜๋„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋ฉ๋‹ˆ๋‹ค. + * + * @property name ํ˜ธ์ถœํ•  ํ•จ์ˆ˜์˜ ์ด๋ฆ„ + * @property args ํ•จ์ˆ˜์— ์ „๋‹ฌ๋  ์ธ์ˆ˜ ๋ชฉ๋ก (AST ๋…ธ๋“œ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast") +data class FunctionCallNode( + val name: String, + val args: List +) : ASTNode() { + + init { + if (name.isBlank()) { + throw ASTException.functionNameEmpty() + } + if (!isValidFunctionName(name)) { + throw ASTException.invalidFunctionName(name) + } + if (args.size > MAX_ARGUMENTS) { + throw ASTException.argumentCountExceeded() + } + } + + override fun getVariables(): Set = args.flatMap { it.getVariables() }.toSet() + + override fun getChildren(): List = args + + override fun isFunctionCall(): Boolean = true + + override fun getDepth(): Int = (args.maxOfOrNull { it.getDepth() } ?: 0) + 1 + + override fun getNodeCount(): Int = args.sumOf { it.getNodeCount() } + 1 + + override fun copy(): FunctionCallNode = FunctionCallNode(name, args.map { it.copy() }) + + override fun toSimpleString(): String = "$name(${args.joinToString(", ") { it.toSimpleString() }})" + + override fun accept(visitor: ASTVisitor): T = visitor.visitFunctionCall(this) + + override fun isStructurallyEqual(other: ASTNode): Boolean = + other is FunctionCallNode && + this.name == other.name && + this.args.size == other.args.size && + this.args.zip(other.args).all { (a, b) -> a.isStructurallyEqual(b) } + + /** + * ํ•จ์ˆ˜๋ช…์ด ์œ ํšจํ•œ ์‹๋ณ„์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param functionName ํ™•์ธํ•  ํ•จ์ˆ˜๋ช… + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + private fun isValidFunctionName(functionName: String): Boolean { + if (functionName.isEmpty()) return false + + // ์ฒซ ๋ฌธ์ž๋Š” ์˜๋ฌธ์ž ๋˜๋Š” ๋ฐ‘์ค„์ด์–ด์•ผ ํ•จ + if (!functionName.first().isLetter() && functionName.first() != '_') return false + + // ๋‚˜๋จธ์ง€ ๋ฌธ์ž๋Š” ์˜๋ฌธ์ž, ์ˆซ์ž, ๋ฐ‘์ค„์ด์–ด์•ผ ํ•จ + return functionName.drop(1).all { it.isLetterOrDigit() || it == '_' } + } + + /** + * ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + */ + fun getArgumentCount(): Int = args.size + + /** + * ์ธ์ˆ˜๊ฐ€ ์—†๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜๊ฐ€ ์—†์œผ๋ฉด true, ์žˆ์œผ๋ฉด false + */ + fun hasNoArguments(): Boolean = args.isEmpty() + + /** + * ๋‹จ์ผ ์ธ์ˆ˜๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ์ผ ์ธ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasSingleArgument(): Boolean = args.size == 1 + + /** + * ๋‹ค์ค‘ ์ธ์ˆ˜๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹ค์ค‘ ์ธ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasMultipleArguments(): Boolean = args.size > 1 + + /** + * ํŠน์ • ์ธ๋ฑ์Šค์˜ ์ธ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ์ธ์ˆ˜ ์ธ๋ฑ์Šค + * @return ํ•ด๋‹น ์ธ๋ฑ์Šค์˜ ์ธ์ˆ˜ + * @throws IndexOutOfBoundsException ์ธ๋ฑ์Šค๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚œ ๊ฒฝ์šฐ + */ + fun getArgument(index: Int): ASTNode { + if (index !in args.indices) { + throw ASTException.indexOutOfRange() + } + return args[index] + } + + /** + * ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฒซ ๋ฒˆ์งธ ์ธ์ˆ˜ + * @throws IllegalStateException ์ธ์ˆ˜๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun getFirstArgument(): ASTNode { + if (args.isEmpty()) { + throw ASTException.argumentsEmpty() + } + return args[0] + } + + /** + * ๋งˆ์ง€๋ง‰ ์ธ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋งˆ์ง€๋ง‰ ์ธ์ˆ˜ + * @throws IllegalStateException ์ธ์ˆ˜๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun getLastArgument(): ASTNode { + if (args.isEmpty()) { + throw ASTException.argumentsEmpty() + } + return args.last() + } + + /** + * ํ•จ์ˆ˜๊ฐ€ ์ˆ˜ํ•™ ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆ˜ํ•™ ํ•จ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isMathFunction(): Boolean = name.lowercase() in MATH_FUNCTIONS + + /** + * ํ•จ์ˆ˜๊ฐ€ ์ง‘๊ณ„ ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง‘๊ณ„ ํ•จ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAggregateFunction(): Boolean = name.lowercase() in AGGREGATE_FUNCTIONS + + /** + * ํ•จ์ˆ˜๊ฐ€ ๋ฌธ์ž์—ด ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฌธ์ž์—ด ํ•จ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isStringFunction(): Boolean = name.lowercase() in STRING_FUNCTIONS + + /** + * ํ•จ์ˆ˜๊ฐ€ ์‚ฌ์šฉ์ž ์ •์˜ ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ฌ์šฉ์ž ์ •์˜ ํ•จ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isUserDefinedFunction(): Boolean = !isMathFunction() && !isAggregateFunction() && !isStringFunction() + + /** + * ํ•จ์ˆ˜์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ + */ + fun getFunctionCategory(): FunctionCategory = when { + isMathFunction() -> FunctionCategory.MATH + isAggregateFunction() -> FunctionCategory.AGGREGATE + isStringFunction() -> FunctionCategory.STRING + else -> FunctionCategory.USER_DEFINED + } + + /** + * ํŠน์ • ์ธ์ˆ˜๋ฅผ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด FunctionCallNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ๊ต์ฒดํ•  ์ธ์ˆ˜์˜ ์ธ๋ฑ์Šค + * @param newArgument ์ƒˆ๋กœ์šด ์ธ์ˆ˜ + * @return ์ƒˆ๋กœ์šด FunctionCallNode + */ + fun withArgument(index: Int, newArgument: ASTNode): FunctionCallNode { + if (index !in args.indices) { + throw ASTException.indexOutOfRange() + } + val newArgs = args.toMutableList() + newArgs[index] = newArgument + return FunctionCallNode(name, newArgs) + } + + /** + * ์ธ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•œ ์ƒˆ๋กœ์šด FunctionCallNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newArgument ์ถ”๊ฐ€ํ•  ์ธ์ˆ˜ + * @return ์ƒˆ๋กœ์šด FunctionCallNode + */ + fun withAddedArgument(newArgument: ASTNode): FunctionCallNode { + if (args.size > MAX_ARGUMENTS) { + throw ASTException.argumentCountExceeded() + } + return FunctionCallNode(name, args + newArgument) + } + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก์„ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด FunctionCallNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newArgs ์ƒˆ๋กœ์šด ์ธ์ˆ˜ ๋ชฉ๋ก + * @return ์ƒˆ๋กœ์šด FunctionCallNode + */ + fun withArguments(newArgs: List): FunctionCallNode = FunctionCallNode(name, newArgs) + + /** + * ํ•จ์ˆ˜๋ช…์„ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด FunctionCallNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newName ์ƒˆ๋กœ์šด ํ•จ์ˆ˜๋ช… + * @return ์ƒˆ๋กœ์šด FunctionCallNode + */ + fun withName(newName: String): FunctionCallNode = FunctionCallNode(newName, args) + + override fun toString(): String = toSimpleString() + + override fun toTreeString(indent: Int): String { + val spaces = " ".repeat(indent) + return buildString { + appendLine("${spaces}FunctionCallNode: $name") + if (args.isNotEmpty()) { + appendLine("${spaces} arguments:") + args.forEachIndexed { index, arg -> + appendLine("${spaces} [$index]:") + if (index < args.size - 1) { + appendLine(arg.toTreeString(indent + 3)) + } else { + append(arg.toTreeString(indent + 3)) + } + } + } else { + append("${spaces} arguments: (none)") + } + } + } + + /** + * ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class FunctionCategory { + MATH, AGGREGATE, STRING, USER_DEFINED + } + + companion object { + /** + * ์ตœ๋Œ€ ์ธ์ˆ˜ ๊ฐœ์ˆ˜์ž…๋‹ˆ๋‹ค. + */ + const val MAX_ARGUMENTS = 10 + + /** + * ์ˆ˜ํ•™ ํ•จ์ˆ˜ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val MATH_FUNCTIONS = setOf( + "sin", "cos", "tan", "asin", "acos", "atan", "atan2", + "sinh", "cosh", "tanh", "asinh", "acosh", "atanh", + "exp", "log", "log10", "log2", "ln", + "sqrt", "cbrt", "pow", "abs", "sign", + "floor", "ceil", "round", "trunc", + "min", "max", "clamp" + ) + + /** + * ์ง‘๊ณ„ ํ•จ์ˆ˜ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val AGGREGATE_FUNCTIONS = setOf( + "sum", "avg", "mean", "median", "mode", + "count", "distinct", "variance", "stddev" + ) + + /** + * ๋ฌธ์ž์—ด ํ•จ์ˆ˜ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val STRING_FUNCTIONS = setOf( + "length", "upper", "lower", "trim", "substring", + "replace", "contains", "startswith", "endswith" + ) + + /** + * ์ง€์›๋˜๋Š” ๋ชจ๋“  ํ•จ์ˆ˜ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ํ•จ์ˆ˜ ์ง‘ํ•ฉ + */ + fun getSupportedFunctions(): Set = + MATH_FUNCTIONS + AGGREGATE_FUNCTIONS + STRING_FUNCTIONS + + /** + * ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ์˜ ํ•จ์ˆ˜ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param category ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ + * @return ํ•ด๋‹น ์นดํ…Œ๊ณ ๋ฆฌ์˜ ํ•จ์ˆ˜ ์ง‘ํ•ฉ + */ + fun getFunctionsByCategory(category: FunctionCategory): Set = when (category) { + FunctionCategory.MATH -> MATH_FUNCTIONS + FunctionCategory.AGGREGATE -> AGGREGATE_FUNCTIONS + FunctionCategory.STRING -> STRING_FUNCTIONS + FunctionCategory.USER_DEFINED -> emptySet() + } + + /** + * ์ธ์ˆ˜ ์—†๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @return FunctionCallNode ์ธ์Šคํ„ด์Šค + */ + fun withoutArguments(name: String): FunctionCallNode = FunctionCallNode(name, emptyList()) + + /** + * ๋‹จ์ผ ์ธ์ˆ˜ ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @param argument ์ธ์ˆ˜ + * @return FunctionCallNode ์ธ์Šคํ„ด์Šค + */ + fun withSingleArgument(name: String, argument: ASTNode): FunctionCallNode = + FunctionCallNode(name, listOf(argument)) + + /** + * ๋‹ค์ค‘ ์ธ์ˆ˜ ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @param arguments ์ธ์ˆ˜๋“ค + * @return FunctionCallNode ์ธ์Šคํ„ด์Šค + */ + fun withArguments(name: String, vararg arguments: ASTNode): FunctionCallNode = + FunctionCallNode(name, arguments.toList()) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/IfNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/IfNode.kt new file mode 100644 index 00000000..546f1db2 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/IfNode.kt @@ -0,0 +1,340 @@ +package hs.kr.entrydsm.domain.ast.entities + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ์กฐ๊ฑด๋ฌธ (IF)์„ ๋‚˜ํƒ€๋‚ด๋Š” AST ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์‚ผํ•ญ ์กฐ๊ฑด ์—ฐ์‚ฐ์ž๋ฅผ ํ‘œํ˜„ํ•˜๋ฉฐ, ์กฐ๊ฑด์‹, ์ฐธ ๊ฐ’, ๊ฑฐ์ง“ ๊ฐ’์œผ๋กœ + * ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. IF(condition, trueValue, falseValue) ํ˜•ํƒœ๋กœ ์‚ฌ์šฉ๋˜๋ฉฐ, + * ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์กฐ๊ฑด๋ถ€ ํ‘œํ˜„์‹์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @property condition ์กฐ๊ฑด์‹ AST ๋…ธ๋“œ + * @property trueValue ์กฐ๊ฑด์ด ์ฐธ์ผ ๋•Œ ํ‰๊ฐ€๋  AST ๋…ธ๋“œ + * @property falseValue ์กฐ๊ฑด์ด ๊ฑฐ์ง“์ผ ๋•Œ ํ‰๊ฐ€๋  AST ๋…ธ๋“œ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast") +data class IfNode( + val condition: ASTNode, + val trueValue: ASTNode, + val falseValue: ASTNode +) : ASTNode() { + + override fun getVariables(): Set = + condition.getVariables() + trueValue.getVariables() + falseValue.getVariables() + + override fun getChildren(): List = listOf(condition, trueValue, falseValue) + + override fun isConditional(): Boolean = true + + override fun getDepth(): Int = maxOf(condition.getDepth(), trueValue.getDepth(), falseValue.getDepth()) + 1 + + override fun getNodeCount(): Int = condition.getNodeCount() + trueValue.getNodeCount() + falseValue.getNodeCount() + 1 + + override fun copy(): IfNode = IfNode(condition.copy(), trueValue.copy(), falseValue.copy()) + + override fun toSimpleString(): String = "$IF(${condition.toSimpleString()}, ${trueValue.toSimpleString()}, ${falseValue.toSimpleString()})" + + override fun accept(visitor: ASTVisitor): T = visitor.visitIf(this) + + override fun isStructurallyEqual(other: ASTNode): Boolean = + other is IfNode && + this.condition.isStructurallyEqual(other.condition) && + this.trueValue.isStructurallyEqual(other.trueValue) && + this.falseValue.isStructurallyEqual(other.falseValue) + + /** + * ์กฐ๊ฑด์‹์ด ์ƒ์ˆ˜ ๊ฐ’์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์กฐ๊ฑด์‹์ด ์ƒ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasConstantCondition(): Boolean = condition.isLiteral() + + /** + * ์กฐ๊ฑด์‹์ด ๋ถˆ๋ฆฐ ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ถˆ๋ฆฐ ๋ฆฌํ„ฐ๋Ÿด์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasBooleanCondition(): Boolean = condition is BooleanNode + + /** + * ์กฐ๊ฑด์‹์ด ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasNumberCondition(): Boolean = condition is NumberNode + + /** + * ์ฐธ ๊ฐ’๊ณผ ๊ฑฐ์ง“ ๊ฐ’์ด ๊ฐ™์€์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ™์œผ๋ฉด true, ๋‹ค๋ฅด๋ฉด false + */ + fun hasSameValues(): Boolean = trueValue.isStructurallyEqual(falseValue) + + /** + * ์ฐธ ๊ฐ’์ด ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฐธ ๊ฐ’์ด ์ƒ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasConstantTrueValue(): Boolean = trueValue.isLiteral() + + /** + * ๊ฑฐ์ง“ ๊ฐ’์ด ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฑฐ์ง“ ๊ฐ’์ด ์ƒ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasConstantFalseValue(): Boolean = falseValue.isLiteral() + + /** + * ๋ชจ๋“  ๊ฐ’์ด ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ชจ๋“  ๊ฐ’์ด ์ƒ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasAllConstantValues(): Boolean = hasConstantCondition() && hasConstantTrueValue() && hasConstantFalseValue() + + /** + * ์ค‘์ฒฉ๋œ IF ๋ฌธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ค‘์ฒฉ๋œ IF ๋ฌธ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isNestedIf(): Boolean = trueValue is IfNode || falseValue is IfNode + + /** + * ์ฐธ ๊ฐ’์ด IF ๋ฌธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฐธ ๊ฐ’์ด IF ๋ฌธ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasTrueValueAsIf(): Boolean = trueValue is IfNode + + /** + * ๊ฑฐ์ง“ ๊ฐ’์ด IF ๋ฌธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฑฐ์ง“ ๊ฐ’์ด IF ๋ฌธ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasFalseValueAsIf(): Boolean = falseValue is IfNode + + /** + * ์กฐ๊ฑด์‹์„ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด IfNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newCondition ์ƒˆ๋กœ์šด ์กฐ๊ฑด์‹ + * @return ์ƒˆ๋กœ์šด IfNode + */ + fun withCondition(newCondition: ASTNode): IfNode = IfNode(newCondition, trueValue, falseValue) + + /** + * ์ฐธ ๊ฐ’์„ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด IfNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newTrueValue ์ƒˆ๋กœ์šด ์ฐธ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด IfNode + */ + fun withTrueValue(newTrueValue: ASTNode): IfNode = IfNode(condition, newTrueValue, falseValue) + + /** + * ๊ฑฐ์ง“ ๊ฐ’์„ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด IfNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newFalseValue ์ƒˆ๋กœ์šด ๊ฑฐ์ง“ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด IfNode + */ + fun withFalseValue(newFalseValue: ASTNode): IfNode = IfNode(condition, trueValue, newFalseValue) + + /** + * ๋ชจ๋“  ๊ตฌ์„ฑ ์š”์†Œ๋ฅผ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด IfNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newCondition ์ƒˆ๋กœ์šด ์กฐ๊ฑด์‹ + * @param newTrueValue ์ƒˆ๋กœ์šด ์ฐธ ๊ฐ’ + * @param newFalseValue ์ƒˆ๋กœ์šด ๊ฑฐ์ง“ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด IfNode + */ + fun withAll(newCondition: ASTNode, newTrueValue: ASTNode, newFalseValue: ASTNode): IfNode = + IfNode(newCondition, newTrueValue, newFalseValue) + + /** + * ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ์ˆœํ™” ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun canSimplify(): Boolean = when { + hasSameValues() -> true + hasBooleanCondition() -> true + hasNumberCondition() && (condition as NumberNode).let { it.isZero() || !it.isZero() } -> true + else -> false + } + + /** + * IF ๋…ธ๋“œ๋ฅผ ๋‹จ์ˆœํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ์ˆœํ™”๋œ AST ๋…ธ๋“œ + * @throws IllegalStateException ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun simplify(): ASTNode { + if (!canSimplify()) { + throw ASTException.ifNotSimplifiable() + } + return when { + hasSameValues() -> trueValue + hasBooleanCondition() -> { + val boolCondition = condition as BooleanNode + if (boolCondition.value) trueValue else falseValue + } + hasNumberCondition() -> { + val numCondition = condition as NumberNode + if (!numCondition.isZero()) trueValue else falseValue + } + else -> throw ASTException.simplificationUnexpectedCase() + } + } + + /** + * IF ๋…ธ๋“œ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ตœ์ ํ™”๋œ AST ๋…ธ๋“œ + */ + fun optimize(): ASTNode { + return when { + canSimplify() -> simplify() + else -> this + } + } + + /** + * ์กฐ๊ฑด์„ ๋ฐ˜์ „์‹œํ‚จ ์ƒˆ๋กœ์šด IfNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์กฐ๊ฑด์ด ๋ฐ˜์ „๋˜๊ณ  ์ฐธ/๊ฑฐ์ง“ ๊ฐ’์ด ๋ฐ”๋€ ์ƒˆ๋กœ์šด IfNode + */ + fun negate(): IfNode { + val negatedCondition = when (condition) { + is BooleanNode -> condition.not() + is UnaryOpNode -> if (condition.isLogicalNot()) condition.operand else UnaryOpNode.logicalNot(condition) + else -> UnaryOpNode.logicalNot(condition) + } + return IfNode(negatedCondition, falseValue, trueValue) + } + + /** + * ์ค‘์ฒฉ ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ค‘์ฒฉ IF ๋ฌธ์˜ ์ตœ๋Œ€ ๊นŠ์ด + */ + override fun getNestingDepth(): Int { + val trueDepth = if (trueValue is IfNode) trueValue.getNestingDepth() + 1 else 1 + val falseDepth = if (falseValue is IfNode) falseValue.getNestingDepth() + 1 else 1 + return maxOf(trueDepth, falseDepth) + } + + /** + * ์‚ผํ•ญ ์—ฐ์‚ฐ์ž ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "condition ? trueValue : falseValue" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toTernaryString(): String = + "${condition.toSimpleString()} ? ${trueValue.toSimpleString()} : ${falseValue.toSimpleString()}" + + override fun toString(): String = toSimpleString() + + override fun toTreeString(indent: Int): String { + val spaces = " ".repeat(indent) + return buildString { + appendLine("${spaces}$IF_NODE") + appendLine("${spaces} $CONDITION") + appendLine(condition.toTreeString(indent + 2)) + appendLine("${spaces} $TRUE_VALUE") + appendLine(trueValue.toTreeString(indent + 2)) + appendLine("${spaces} $FALSE_VALUE") + append(falseValue.toTreeString(indent + 2)) + } + } + + companion object { + + const val IF = "IF" + const val IF_NODE = "IfNode:" + const val CONDITION = "condition:" + const val TRUE_VALUE = "trueValue:" + const val FALSE_VALUE = "falseValue:" + const val LOGICAL_AND = "&&" + + /** + * ๋ถˆ๋ฆฐ ์กฐ๊ฑด์œผ๋กœ IF ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param condition ๋ถˆ๋ฆฐ ์กฐ๊ฑด + * @param trueValue ์ฐธ ๊ฐ’ + * @param falseValue ๊ฑฐ์ง“ ๊ฐ’ + * @return IfNode ์ธ์Šคํ„ด์Šค + */ + fun withBooleanCondition(condition: Boolean, trueValue: ASTNode, falseValue: ASTNode): IfNode = + IfNode(BooleanNode.of(condition), trueValue, falseValue) + + /** + * ์ˆซ์ž ์กฐ๊ฑด์œผ๋กœ IF ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param condition ์ˆซ์ž ์กฐ๊ฑด (0์ด๋ฉด ๊ฑฐ์ง“, ๊ทธ ์™ธ๋Š” ์ฐธ) + * @param trueValue ์ฐธ ๊ฐ’ + * @param falseValue ๊ฑฐ์ง“ ๊ฐ’ + * @return IfNode ์ธ์Šคํ„ด์Šค + */ + fun withNumberCondition(condition: Double, trueValue: ASTNode, falseValue: ASTNode): IfNode = + IfNode(NumberNode.of(condition), trueValue, falseValue) + + /** + * ๋‹จ์ˆœ ๋ถˆ๋ฆฐ ๋ถ„๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param condition ์กฐ๊ฑด์‹ + * @param trueValue ์ฐธ์ผ ๋•Œ ๊ฐ’ + * @param falseValue ๊ฑฐ์ง“์ผ ๋•Œ ๊ฐ’ + * @return IfNode ์ธ์Šคํ„ด์Šค + */ + fun createBranch(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode): IfNode = + IfNode(condition, trueValue, falseValue) + + /** + * ์ค‘์ฒฉ IF ๋ฌธ์„ ํ‰๋ฉดํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ifNode ํ‰๋ฉดํ™”ํ•  IF ๋…ธ๋“œ + * @return ํ‰๋ฉดํ™”๋œ IF ์กฐ๊ฑด๋“ค์˜ ๋ฆฌ์ŠคํŠธ + */ + fun flatten(ifNode: IfNode): List> { + val result = mutableListOf>() + + fun collect(node: IfNode, currentConditions: List) { + val trueConditions = currentConditions + node.condition + val falseConditions = currentConditions + UnaryOpNode.logicalNot(node.condition) + + when (node.trueValue) { + is IfNode -> collect(node.trueValue, trueConditions) + else -> result.add(createCompoundCondition(trueConditions) to node.trueValue) + } + + when (node.falseValue) { + is IfNode -> collect(node.falseValue, falseConditions) + else -> result.add(createCompoundCondition(falseConditions) to node.falseValue) + } + } + + collect(ifNode, emptyList()) + return result + } + + /** + * ์กฐ๊ฑด ๋ฆฌ์ŠคํŠธ๋ฅผ ํ•˜๋‚˜์˜ ๋ณตํ•ฉ ์กฐ๊ฑด์œผ๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param conditions ๊ฒฐํ•ฉํ•  ์กฐ๊ฑด๋“ค + * @return ๊ฒฐํ•ฉ๋œ ์กฐ๊ฑด + */ + private fun createCompoundCondition(conditions: List): ASTNode { + return when { + conditions.isEmpty() -> BooleanNode.TRUE + conditions.size == 1 -> conditions.first() + else -> conditions.reduce { acc, condition -> + BinaryOpNode(acc, LOGICAL_AND, condition) + } + } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/NumberNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/NumberNode.kt new file mode 100644 index 00000000..1dc3d3c7 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/NumberNode.kt @@ -0,0 +1,254 @@ +package hs.kr.entrydsm.domain.ast.entities + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด์„ ๋‚˜ํƒ€๋‚ด๋Š” AST ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“  ์ˆซ์ž ๊ฐ’(์ •์ˆ˜, ์‹ค์ˆ˜)์„ ํ‘œํ˜„ํ•˜๋ฉฐ, + * Double ํƒ€์ž…์œผ๋กœ ๊ฐ’์„ ์ €์žฅํ•˜์—ฌ ์ •๋ฐ€ํ•œ ๊ณ„์‚ฐ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋˜์–ด ์•ˆ์ „ํ•œ ๊ฐ’ ์ „๋‹ฌ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @property value ๋…ธ๋“œ์˜ ์ˆซ์ž ๊ฐ’ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast") +data class NumberNode(val value: Double) : ASTNode() { + + init { + if (!value.isFinite()) { + throw ASTException.numberNotFinite(value) + } + } + + override fun getVariables(): Set = emptySet() + + override fun getChildren(): List = emptyList() + + override fun isLiteral(): Boolean = true + + override fun getDepth(): Int = 1 + + override fun getNodeCount(): Int = 1 + + override fun copy(): NumberNode = NumberNode(value) + + override fun toSimpleString(): String = when { + value == value.toLong().toDouble() -> value.toLong().toString() + else -> value.toString() + } + + override fun accept(visitor: ASTVisitor): T = visitor.visitNumber(this) + + override fun isStructurallyEqual(other: ASTNode): Boolean = + other is NumberNode && areNumbersEqual(this.value, other.value) + + /** + * ๋‘ double ๊ฐ’์ด ์˜๋ฏธ์ ์œผ๋กœ ๊ฐ™์€์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * ๋ถ€๋™์†Œ์ˆ˜์  ์ •๋ฐ€๋„ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๊ธฐ ์œ„ํ•ด ์—ก์‹ค๋ก  ๊ธฐ๋ฐ˜ ๋น„๊ต๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param a ์ฒซ ๋ฒˆ์งธ ๊ฐ’ + * @param b ๋‘ ๋ฒˆ์งธ ๊ฐ’ + * @return ๊ฐ’์ด ๊ฐ™์œผ๋ฉด true + */ + private fun areNumbersEqual(a: Double, b: Double): Boolean { + // NaN๊ณผ ๋ฌดํ•œ๋Œ€๋Š” ์ •ํ™•ํžˆ ๊ฐ™์•„์•ผ ํ•จ + if (a.isNaN() && b.isNaN()) return true + if (a.isInfinite() && b.isInfinite()) return a == b + if (a.isNaN() || b.isNaN()) return false + if (a.isInfinite() || b.isInfinite()) return false + + // 0์— ๊ฐ€๊นŒ์šด ๊ฐ’๋“ค์€ ์ ˆ๋Œ€ ์ฐจ์ด๋กœ ๋น„๊ต + if (kotlin.math.abs(a) < EPSILON && kotlin.math.abs(b) < EPSILON) { + return kotlin.math.abs(a - b) < EPSILON + } + + // ์ผ๋ฐ˜์ ์ธ ๊ฒฝ์šฐ๋Š” ์ƒ๋Œ€ ์˜ค์ฐจ๋กœ ๋น„๊ต + val diff = kotlin.math.abs(a - b) + val maxAbs = kotlin.math.max(kotlin.math.abs(a), kotlin.math.abs(b)) + return diff <= EPSILON * maxAbs + } + + /** + * ์ˆซ์ž ๊ฐ’์ด ์ •์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ •์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isInteger(): Boolean = value == value.toLong().toDouble() + + /** + * ์ˆซ์ž ๊ฐ’์ด ์–‘์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์–‘์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isPositive(): Boolean = value > 0.0 + + /** + * ์ˆซ์ž ๊ฐ’์ด ์Œ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์Œ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isNegative(): Boolean = value < 0.0 + + /** + * ์ˆซ์ž ๊ฐ’์ด 0์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return 0์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isZero(): Boolean = value == 0.0 + + /** + * ์ˆซ์ž ๊ฐ’์„ ์ •์ˆ˜๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ •์ˆ˜ ๊ฐ’ + * @throws IllegalStateException ์ •์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ + */ + fun toInt(): Int { + if (!isInteger()) { + throw ASTException.notIntegerForInt(value) + } + return value.toInt() + } + + /** + * ์ˆซ์ž ๊ฐ’์„ Long์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Long ๊ฐ’ + * @throws IllegalStateException ์ •์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ + */ + fun toLong(): Long { + if (!isInteger()) { + throw ASTException.notIntegerForLong(value) + } + return value.toLong() + } + + /** + * ์ˆซ์ž์˜ ์ ˆ๋Œ“๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ ˆ๋Œ“๊ฐ’์„ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด NumberNode + */ + fun abs(): NumberNode = NumberNode(kotlin.math.abs(value)) + + /** + * ์ˆซ์ž์˜ ๋ถ€ํ˜ธ๋ฅผ ๋ฐ˜์ „ํ•œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ถ€ํ˜ธ๊ฐ€ ๋ฐ˜์ „๋œ ์ƒˆ๋กœ์šด NumberNode + */ + fun negate(): NumberNode = NumberNode(-value) + + /** + * ๋‹ค๋ฅธ NumberNode์™€ ๋”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋”ํ•  NumberNode + * @return ํ•ฉ์„ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด NumberNode + */ + operator fun plus(other: NumberNode): NumberNode = NumberNode(value + other.value) + + /** + * ๋‹ค๋ฅธ NumberNode์™€ ๋บ๋‹ˆ๋‹ค. + * + * @param other ๋บ„ NumberNode + * @return ์ฐจ๋ฅผ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด NumberNode + */ + operator fun minus(other: NumberNode): NumberNode = NumberNode(value - other.value) + + /** + * ๋‹ค๋ฅธ NumberNode์™€ ๊ณฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๊ณฑํ•  NumberNode + * @return ๊ณฑ์„ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด NumberNode + */ + operator fun times(other: NumberNode): NumberNode = NumberNode(value * other.value) + + /** + * ๋‹ค๋ฅธ NumberNode๋กœ ๋‚˜๋ˆ•๋‹ˆ๋‹ค. + * + * @param other ๋‚˜๋ˆŒ NumberNode + * @return ๋ชซ์„ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด NumberNode + */ + operator fun div(other: NumberNode): NumberNode { + if (other.isZero()) { + throw ASTException.divisionByZero() + } + return NumberNode(value / other.value) + } + + /** + * ๋‹ค๋ฅธ NumberNode์™€ ํฌ๊ธฐ๋ฅผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  NumberNode + * @return ๋น„๊ต ๊ฒฐ๊ณผ (-1, 0, 1) + */ + operator fun compareTo(other: NumberNode): Int = value.compareTo(other.value) + + override fun toString(): String = toSimpleString() + + override fun toTreeString(indent: Int): String { + val spaces = " ".repeat(indent) + return "${spaces}$NUMBER_NODE: $value" + } + + companion object { + + const val NUMBER_NODE = "NumberNode:" + /** + * ๋ถ€๋™์†Œ์ˆ˜์  ๋น„๊ต๋ฅผ ์œ„ํ•œ ์—ก์‹ค๋ก  ๊ฐ’ + */ + private const val EPSILON = 1e-10 + + /** + * 0์„ ๋‚˜ํƒ€๋‚ด๋Š” NumberNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + val ZERO = NumberNode(0.0) + + /** + * 1์„ ๋‚˜ํƒ€๋‚ด๋Š” NumberNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + val ONE = NumberNode(1.0) + + /** + * -1์„ ๋‚˜ํƒ€๋‚ด๋Š” NumberNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + val MINUS_ONE = NumberNode(-1.0) + + /** + * ์ •์ˆ˜ ๊ฐ’์œผ๋กœ NumberNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ •์ˆ˜ ๊ฐ’ + * @return NumberNode ์ธ์Šคํ„ด์Šค + */ + fun of(value: Int): NumberNode = NumberNode(value.toDouble()) + + /** + * Long ๊ฐ’์œผ๋กœ NumberNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value Long ๊ฐ’ + * @return NumberNode ์ธ์Šคํ„ด์Šค + */ + fun of(value: Long): NumberNode = NumberNode(value.toDouble()) + + /** + * Double ๊ฐ’์œผ๋กœ NumberNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value Double ๊ฐ’ + * @return NumberNode ์ธ์Šคํ„ด์Šค + */ + fun of(value: Double): NumberNode = NumberNode(value) + + /** + * ๋ฌธ์ž์—ด๋กœ๋ถ€ํ„ฐ NumberNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ˆซ์ž ๋ฌธ์ž์—ด + * @return NumberNode ์ธ์Šคํ„ด์Šค + * @throws NumberFormatException ์ž˜๋ชป๋œ ์ˆซ์ž ํ˜•์‹์ธ ๊ฒฝ์šฐ + */ + fun parse(value: String): NumberNode = NumberNode(value.toDouble()) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/UnaryOpNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/UnaryOpNode.kt new file mode 100644 index 00000000..f2a34824 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/UnaryOpNode.kt @@ -0,0 +1,296 @@ +package hs.kr.entrydsm.domain.ast.entities + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ๋‹จํ•ญ ์—ฐ์‚ฐ(์˜ˆ: ์Œ์ˆ˜, ๋…ผ๋ฆฌ ๋ถ€์ •)์„ ๋‚˜ํƒ€๋‚ด๋Š” AST ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋“  ๋‹จํ•ญ ์—ฐ์‚ฐ์ž๋ฅผ ํ‘œํ˜„ํ•˜๋ฉฐ, ์—ฐ์‚ฐ์ž์™€ + * ํ”ผ์—ฐ์‚ฐ์ž๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์Œ์ˆ˜ ์—ฐ์‚ฐ์ž(-), ๋…ผ๋ฆฌ ๋ถ€์ • ์—ฐ์‚ฐ์ž(!) ๋“ฑ์„ + * ์ง€์›ํ•˜๋ฉฐ, ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋˜์–ด ์•ˆ์ „ํ•œ ์—ฐ์‚ฐ ํŠธ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @property operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด (์˜ˆ: "-", "!") + * @property operand ํ”ผ์—ฐ์‚ฐ์ž AST ๋…ธ๋“œ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast") +data class UnaryOpNode( + val operator: String, + val operand: ASTNode +) : ASTNode() { + + init { + if (operator.isBlank()) { + throw ASTException.operatorEmpty() + } + + if (!isSupportedOperator(operator)) { + throw ASTException.unsupportedUnaryOperator(operator) + } + } + + override fun getVariables(): Set = operand.getVariables() + + override fun getChildren(): List = listOf(operand) + + override fun isOperator(): Boolean = true + + override fun getDepth(): Int = operand.getDepth() + 1 + + override fun getNodeCount(): Int = operand.getNodeCount() + 1 + + override fun copy(): UnaryOpNode = UnaryOpNode(operator, operand.copy()) + + override fun toSimpleString(): String = "$operator$operand" + + override fun accept(visitor: ASTVisitor): T = visitor.visitUnaryOp(this) + + override fun isStructurallyEqual(other: ASTNode): Boolean = + other is UnaryOpNode && + this.operator == other.operator && + this.operand.isStructurallyEqual(other.operand) + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ์ง€์›๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param op ํ™•์ธํ•  ์—ฐ์‚ฐ์ž + * @return ์ง€์›๋˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + private fun isSupportedOperator(op: String): Boolean = op in SUPPORTED_OPERATORS + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ์‚ฐ์ˆ  ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ฐ์ˆ  ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isArithmeticOperator(): Boolean = operator in ARITHMETIC_OPERATORS + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ๋…ผ๋ฆฌ ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋…ผ๋ฆฌ ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isLogicalOperator(): Boolean = operator in LOGICAL_OPERATORS + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ์Œ์ˆ˜ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์Œ์ˆ˜ ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isNegation(): Boolean = operator == MINUS + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ๋…ผ๋ฆฌ ๋ถ€์ • ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋…ผ๋ฆฌ ๋ถ€์ • ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isLogicalNot(): Boolean = operator == EXCLAMATION + + /** + * ์—ฐ์‚ฐ์ž๊ฐ€ ์–‘์ˆ˜ ํ‘œ์‹œ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์–‘์ˆ˜ ํ‘œ์‹œ ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isPositive(): Boolean = operator == PLUS + + /** + * ์—ฐ์‚ฐ์ž์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ๋จผ์ € ๊ณ„์‚ฐ) + */ + fun getPrecedence(): Int = OPERATOR_PRECEDENCE[operator] ?: 0 + + /** + * ์—ฐ์‚ฐ์ž์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฐ์‚ฐ์ž ํƒ€์ž… + */ + fun getOperatorType(): OperatorType = when { + isArithmeticOperator() -> OperatorType.ARITHMETIC + isLogicalOperator() -> OperatorType.LOGICAL + else -> OperatorType.UNKNOWN + } + + /** + * ํ”ผ์—ฐ์‚ฐ์ž๋ฅผ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด UnaryOpNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newOperand ์ƒˆ๋กœ์šด ํ”ผ์—ฐ์‚ฐ์ž + * @return ์ƒˆ๋กœ์šด UnaryOpNode + */ + fun withOperand(newOperand: ASTNode): UnaryOpNode = UnaryOpNode(operator, newOperand) + + /** + * ์—ฐ์‚ฐ์ž๋ฅผ ๊ต์ฒดํ•œ ์ƒˆ๋กœ์šด UnaryOpNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newOperator ์ƒˆ๋กœ์šด ์—ฐ์‚ฐ์ž + * @return ์ƒˆ๋กœ์šด UnaryOpNode + */ + fun withOperator(newOperator: String): UnaryOpNode = UnaryOpNode(newOperator, operand) + + /** + * ์ด์ค‘ ์Œ์ˆ˜(-(-x))๋ฅผ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ์ˆœํ™” ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun canSimplifyDoubleNegation(): Boolean = + isNegation() && operand is UnaryOpNode && operand.isNegation() + + /** + * ์ด์ค‘ ๋ถ€์ •(!(!x))๋ฅผ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ์ˆœํ™” ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun canSimplifyDoubleNegationLogical(): Boolean = + isLogicalNot() && operand is UnaryOpNode && operand.isLogicalNot() + + /** + * ์ด์ค‘ ์Œ์ˆ˜๋ฅผ ๋‹จ์ˆœํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ์ˆœํ™”๋œ AST ๋…ธ๋“œ + * @throws IllegalStateException ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun simplifyDoubleNegation(): ASTNode { + if (!canSimplifyDoubleNegation()) { + throw ASTException.doubleNegationNotSimplifiable() + } + return (operand as UnaryOpNode).operand + } + + /** + * ์ด์ค‘ ๋…ผ๋ฆฌ ๋ถ€์ •์„ ๋‹จ์ˆœํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ์ˆœํ™”๋œ AST ๋…ธ๋“œ + * @throws IllegalStateException ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun simplifyDoubleLogicalNegation(): ASTNode { + if (!canSimplifyDoubleNegationLogical()) { + throw ASTException.doubleLogicalNegationNotSimplifiable() + } + return (operand as UnaryOpNode).operand + } + + /** + * UnaryOpNode๋ฅผ ๋‹จ์ˆœํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ์ˆœํ™”๋œ AST ๋…ธ๋“œ + */ + fun simplify(): ASTNode { + return when { + canSimplifyDoubleNegation() -> simplifyDoubleNegation() + canSimplifyDoubleNegationLogical() -> simplifyDoubleLogicalNegation() + else -> this + } + } + + /** + * ๊ด„ํ˜ธ๋ฅผ ํฌํ•จํ•œ ๋ฌธ์ž์—ด ํ‘œํ˜„์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ด„ํ˜ธ๊ฐ€ ํฌํ•จ๋œ ๋ฌธ์ž์—ด + */ + fun toStringWithParentheses(): String { + val operandStr = when { + operand is BinaryOpNode -> "(${operand.toSimpleString()})" + operand is UnaryOpNode && operand.getPrecedence() <= getPrecedence() -> "(${operand.toSimpleString()})" + else -> operand.toSimpleString() + } + return "$operator$operandStr" + } + + override fun toString(): String = toStringWithParentheses() + + override fun toTreeString(indent: Int): String { + val spaces = " ".repeat(indent) + return buildString { + appendLine("${spaces}UnaryOpNode: $operator") + appendLine("${spaces} operand:") + append(operand.toTreeString(indent + 2)) + } + } + + /** + * ์—ฐ์‚ฐ์ž ํƒ€์ž…์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class OperatorType { + ARITHMETIC, LOGICAL, UNKNOWN + } + + companion object { + + const val MINUS = "-" + const val PLUS = "+" + const val EXCLAMATION = "!" + + /** + * ์ง€์›๋˜๋Š” ๋ชจ๋“  ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val SUPPORTED_OPERATORS = setOf("-", "!", "+") + + /** + * ์‚ฐ์ˆ  ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val ARITHMETIC_OPERATORS = setOf("-", "+") + + /** + * ๋…ผ๋ฆฌ ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val LOGICAL_OPERATORS = setOf("!") + + /** + * ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๋งต์ž…๋‹ˆ๋‹ค. + */ + private val OPERATOR_PRECEDENCE = mapOf( + "!" to 8, // ์ตœ๊ณ  ์šฐ์„ ์ˆœ์œ„ + "-" to 8, // ๋‹จํ•ญ ๋งˆ์ด๋„ˆ์Šค + "+" to 8 // ๋‹จํ•ญ ํ”Œ๋Ÿฌ์Šค + ) + + /** + * ์ง€์›๋˜๋Š” ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ์—ฐ์‚ฐ์ž ์ง‘ํ•ฉ + */ + fun getSupportedOperators(): Set = SUPPORTED_OPERATORS.toSet() + + /** + * ํŠน์ • ํƒ€์ž…์˜ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ์—ฐ์‚ฐ์ž ํƒ€์ž… + * @return ํ•ด๋‹น ํƒ€์ž…์˜ ์—ฐ์‚ฐ์ž ์ง‘ํ•ฉ + */ + fun getOperatorsByType(type: OperatorType): Set = when (type) { + OperatorType.ARITHMETIC -> ARITHMETIC_OPERATORS + OperatorType.LOGICAL -> LOGICAL_OPERATORS + OperatorType.UNKNOWN -> emptySet() + } + + /** + * ์Œ์ˆ˜ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operand ํ”ผ์—ฐ์‚ฐ์ž + * @return ์Œ์ˆ˜ UnaryOpNode + */ + fun negate(operand: ASTNode): UnaryOpNode = UnaryOpNode("-", operand) + + /** + * ๋…ผ๋ฆฌ ๋ถ€์ • ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operand ํ”ผ์—ฐ์‚ฐ์ž + * @return ๋…ผ๋ฆฌ ๋ถ€์ • UnaryOpNode + */ + fun logicalNot(operand: ASTNode): UnaryOpNode = UnaryOpNode("!", operand) + + /** + * ์–‘์ˆ˜ ํ‘œ์‹œ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operand ํ”ผ์—ฐ์‚ฐ์ž + * @return ์–‘์ˆ˜ ํ‘œ์‹œ UnaryOpNode + */ + fun positive(operand: ASTNode): UnaryOpNode = UnaryOpNode("+", operand) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/VariableNode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/VariableNode.kt new file mode 100644 index 00000000..6f244439 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/entities/VariableNode.kt @@ -0,0 +1,221 @@ +package hs.kr.entrydsm.domain.ast.entities + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ๋ณ€์ˆ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” AST ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ณ€์ˆ˜๋ฅผ ํ‘œํ˜„ํ•˜๋ฉฐ, ์ค‘๊ด„ํ˜ธ๋กœ ๋ฌถ์ธ ํ˜•ํƒœ({variable})๋กœ + * ์ž…๋ ฅ๋ฉ๋‹ˆ๋‹ค. ๋ณ€์ˆ˜๋ช…์€ ์‹๋ณ„์ž ๊ทœ์น™์„ ๋”ฐ๋ผ์•ผ ํ•˜๋ฉฐ, ํ‰๊ฐ€ ์‹œ ๋ณ€์ˆ˜ ๊ฐ’์œผ๋กœ ๋Œ€์ฒด๋ฉ๋‹ˆ๋‹ค. + * ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋˜์–ด ์•ˆ์ „ํ•œ ๋ณ€์ˆ˜ ์ฐธ์กฐ๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @property name ๋ณ€์ˆ˜์˜ ์ด๋ฆ„ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(aggregateRoot = hs.kr.entrydsm.domain.ast.aggregates.ExpressionAST::class, context = "ast") +data class VariableNode(val name: String) : ASTNode() { + + init { + if (name.isBlank()) { + throw ASTException.variableNameEmpty() + } + + if (!isValidVariableName(name)) { + throw ASTException.invalidVariableName(name) + } } + + override fun getVariables(): Set = setOf(name) + + override fun getChildren(): List = emptyList() + + override fun isVariable(): Boolean = true + + override fun getDepth(): Int = 1 + + override fun getNodeCount(): Int = 1 + + override fun copy(): VariableNode = VariableNode(name) + + override fun toSimpleString(): String = name + + override fun accept(visitor: ASTVisitor): T = visitor.visitVariable(this) + + override fun isStructurallyEqual(other: ASTNode): Boolean = + other is VariableNode && this.name == other.name + + /** + * ๋ณ€์ˆ˜๋ช…์ด ์œ ํšจํ•œ ์‹๋ณ„์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param variableName ํ™•์ธํ•  ๋ณ€์ˆ˜๋ช… + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + private fun isValidVariableName(variableName: String): Boolean = isValidName(variableName) + + /** + * ๋ณ€์ˆ˜๋ช…์ด ํ‚ค์›Œ๋“œ์™€ ์ถฉ๋Œํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ‚ค์›Œ๋“œ์™€ ์ถฉ๋Œํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isKeywordConflict(): Boolean = name.uppercase() in RESERVED_KEYWORDS + + /** + * ๋ณ€์ˆ˜๋ช…์ด ๋Œ€์†Œ๋ฌธ์ž๋ฅผ ๊ตฌ๋ถ„ํ•˜์ง€ ์•Š๊ณ  ๋‹ค๋ฅธ ๋ณ€์ˆ˜์™€ ๊ฐ™์€์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๋ณ€์ˆ˜๋ช… + * @return ๋Œ€์†Œ๋ฌธ์ž ๋ฌด๊ด€ํ•˜๊ฒŒ ๊ฐ™์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSameVariableIgnoreCase(other: String): Boolean = + name.equals(other, ignoreCase = true) + + /** + * ๋ณ€์ˆ˜๋ช…์ด ๋‹ค๋ฅธ VariableNode์™€ ๊ฐ™์€์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  VariableNode + * @return ๊ฐ™์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSameVariable(other: VariableNode): Boolean = name == other.name + + /** + * ๋ณ€์ˆ˜๋ช…์ด ๋‹ค๋ฅธ VariableNode์™€ ๋Œ€์†Œ๋ฌธ์ž ๋ฌด๊ด€ํ•˜๊ฒŒ ๊ฐ™์€์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  VariableNode + * @return ๋Œ€์†Œ๋ฌธ์ž ๋ฌด๊ด€ํ•˜๊ฒŒ ๊ฐ™์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSameVariableIgnoreCase(other: VariableNode): Boolean = + isSameVariableIgnoreCase(other.name) + + /** + * ๋ณ€์ˆ˜๋ช…์˜ ๊ธธ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€์ˆ˜๋ช… ๊ธธ์ด + */ + fun getNameLength(): Int = name.length + + /** + * ๋ณ€์ˆ˜๋ช…์ด ํŠน์ • ์ ‘๋‘์‚ฌ๋กœ ์‹œ์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param prefix ํ™•์ธํ•  ์ ‘๋‘์‚ฌ + * @return ์ ‘๋‘์‚ฌ๋กœ ์‹œ์ž‘ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasPrefix(prefix: String): Boolean = name.startsWith(prefix) + + /** + * ๋ณ€์ˆ˜๋ช…์ด ํŠน์ • ์ ‘๋ฏธ์‚ฌ๋กœ ๋๋‚˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param suffix ํ™•์ธํ•  ์ ‘๋ฏธ์‚ฌ + * @return ์ ‘๋ฏธ์‚ฌ๋กœ ๋๋‚˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasSuffix(suffix: String): Boolean = name.endsWith(suffix) + + /** + * ๋ณ€์ˆ˜๋ช…์— ํŠน์ • ๋ฌธ์ž์—ด์ด ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param substring ํ™•์ธํ•  ๋ถ€๋ถ„ ๋ฌธ์ž์—ด + * @return ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun contains(substring: String): Boolean = name.contains(substring) + + /** + * ๋ณ€์ˆ˜๋ช…์„ ์†Œ๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•œ ์ƒˆ๋กœ์šด VariableNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์†Œ๋ฌธ์ž ๋ณ€์ˆ˜๋ช…์„ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด VariableNode + */ + fun toLowerCase(): VariableNode = VariableNode(name.lowercase()) + + /** + * ๋ณ€์ˆ˜๋ช…์„ ๋Œ€๋ฌธ์ž๋กœ ๋ณ€ํ™˜ํ•œ ์ƒˆ๋กœ์šด VariableNode๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋Œ€๋ฌธ์ž ๋ณ€์ˆ˜๋ช…์„ ๊ฐ€์ง„ ์ƒˆ๋กœ์šด VariableNode + */ + fun toUpperCase(): VariableNode = VariableNode(name.uppercase()) + + /** + * ๋ณ€์ˆ˜๋ฅผ ์ค‘๊ด„ํ˜ธ ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "{๋ณ€์ˆ˜๋ช…}" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toBracketedString(): String = "{$name}" + + override fun toString(): String = toBracketedString() + + override fun toTreeString(indent: Int): String { + val spaces = " ".repeat(indent) + return "${spaces}VariableNode: {$name}" + } + + companion object { + /** + * ์˜ˆ์•ฝ๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val RESERVED_KEYWORDS = setOf( + "IF", "TRUE", "FALSE", "AND", "OR", "NOT" + ) + + /** + * ๋ณ€์ˆ˜๋ช…์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๊ฒ€์ฆํ•  ๋ณ€์ˆ˜๋ช… + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValidName(name: String): Boolean { + if (name.isBlank()) return false + if (!name.first().isLetter() && name.first() != '_') return false + return name.drop(1).all { it.isLetterOrDigit() || it == '_' } + } + + /** + * ์•ˆ์ „ํ•œ ๋ฐฉ์‹์œผ๋กœ VariableNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜๋ช… + * @return VariableNode ์ธ์Šคํ„ด์Šค ๋˜๋Š” null (์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ) + */ + fun createSafe(name: String): VariableNode? = try { + if (isValidName(name)) VariableNode(name) else null + } catch (e: IllegalArgumentException) { + // ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช…์œผ๋กœ ์ธํ•œ ์˜ˆ์ƒ๋œ ์˜ˆ์™ธ๋Š” ์กฐ์šฉํžˆ ์ฒ˜๋ฆฌ + null + } catch (e: Exception) { + // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ˆ์™ธ๋Š” ๊ฐ„๋‹จํ•˜๊ฒŒ ๋กœ๊น…ํ•˜๊ณ  null ๋ฐ˜ํ™˜ + System.err.println("์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: name='$name', error=${e.javaClass.simpleName}: ${e.message}") + null + } + + /** + * ์—ฌ๋Ÿฌ ๋ณ€์ˆ˜๋ช…์œผ๋กœ๋ถ€ํ„ฐ VariableNode ๋ฆฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param names ๋ณ€์ˆ˜๋ช… ๋ฆฌ์ŠคํŠธ + * @return ์œ ํšจํ•œ VariableNode ๋ฆฌ์ŠคํŠธ + */ + fun createFromNames(names: List): List = + names.mapNotNull { createSafe(it) } + + /** + * ๋ฌธ์ž์—ด์—์„œ ๋ณ€์ˆ˜๋ช…์„ ์ถ”์ถœํ•˜์—ฌ VariableNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param bracketedString "{๋ณ€์ˆ˜๋ช…}" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + * @return VariableNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์ž˜๋ชป๋œ ํ˜•์‹์ธ ๊ฒฝ์šฐ + */ + fun fromBracketedString(bracketedString: String): VariableNode { + if (!(bracketedString.startsWith("{") && bracketedString.endsWith("}"))) { + throw ASTException.variableNotBracketed(bracketedString) + } + + val variableName = bracketedString.substring(1, bracketedString.length - 1) + return VariableNode(variableName) + } + + /** + * ์˜ˆ์•ฝ๋œ ํ‚ค์›Œ๋“œ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ˆ์•ฝ๋œ ํ‚ค์›Œ๋“œ ์ง‘ํ•ฉ + */ + fun getReservedKeywords(): Set = RESERVED_KEYWORDS.toSet() + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/events/DomainEvents.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/events/DomainEvents.kt new file mode 100644 index 00000000..b2a86258 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/events/DomainEvents.kt @@ -0,0 +1,28 @@ +package hs.kr.entrydsm.domain.ast.events + +/** + * AST ๋„๋ฉ”์ธ์—์„œ ๋ฐœํ–‰/๊ตฌ๋…์— ์‚ฌ์šฉํ•˜๋Š” ์ด๋ฒคํŠธ ํ‚ค ์ƒ์ˆ˜ ๋ชจ์Œ. + * + * ๋ฉ”์‹œ์ง€ ๋ธŒ๋กœ์ปค, ์ด๋ฒคํŠธ ๋ฒ„์Šค, ๋กœ๊น… ๋“ฑ์—์„œ **์ผ๊ด€๋œ ์ด๋ฒคํŠธ ์ด๋ฆ„**์œผ๋กœ ํ™œ์šฉํ•˜์„ธ์š”. + * (๊ถŒ์žฅ ๊ณตํ†ต ํŽ˜์ด๋กœ๋“œ: `aggregateId`, `occurredAt`, `actor`, `metadata`) + */ +object DomainEvents { + + /** AST(์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ)๊ฐ€ ์ตœ์ดˆ ์ƒ์„ฑ๋˜์—ˆ์„ ๋•Œ. */ + const val AST_CREATED = "AST_CREATED" + + /** AST๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ(๋…ธ๋“œ ์ถ”๊ฐ€/์‚ญ์ œ/๊ฐฑ์‹  ๋“ฑ). */ + const val AST_MODIFIED = "AST_MODIFIED" + + /** AST์˜ ๋ถ€๋ถ„ ํŠธ๋ฆฌ๊ฐ€ ๋‹ค๋ฅธ ํŠธ๋ฆฌ๋กœ ๊ต์ฒด๋˜์—ˆ์„ ๋•Œ. */ + const val SUBTREE_REPLACED = "SUBTREE_REPLACED" + + /** ExpressionAST ๋„๋ฉ”์ธ์šฉ ํ† ํ”ฝ/์นดํ…Œ๊ณ ๋ฆฌ ์‹๋ณ„์ž. */ + const val EXPRESSION_AST = "ExpressionAST" + + /** AST ์ตœ์ ํ™”(์ƒ์ˆ˜ ํด๋”ฉ, ๋ถˆํ•„์š” ๋…ธ๋“œ ์ œ๊ฑฐ ๋“ฑ) ์™„๋ฃŒ ์‹œ. */ + const val AST_OPTIMIZED = "AST_OPTIMIZED" + + /** AST ์œ ํšจ์„ฑ ๊ฒ€์ฆ์„ ํ†ต๊ณผํ–ˆ์„ ๋•Œ. */ + const val AST_VALIDATED = "AST_VALIDATED" +} diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/exceptions/ASTException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/exceptions/ASTException.kt new file mode 100644 index 00000000..9e7568fe --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/exceptions/ASTException.kt @@ -0,0 +1,1207 @@ +package hs.kr.entrydsm.domain.ast.exceptions + +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.global.exception.DomainException + +/** + * AST(Abstract Syntax Tree) ๋„๋ฉ”์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * AST ๊ตฌ์ถ•, ๋…ธ๋“œ ํƒ€์ž… ๊ฒ€์ฆ, ํŠธ๋ฆฌ ๊ตฌ์กฐ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋“ฑ์˜ + * ์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ ๊ด€๋ จ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @property nodeType ์˜ค๋ฅ˜์™€ ๊ด€๋ จ๋œ ๋…ธ๋“œ ํƒ€์ž… (์„ ํƒ์‚ฌํ•ญ) + * @property nodeName ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๋…ธ๋“œ ์ด๋ฆ„ (์„ ํƒ์‚ฌํ•ญ) + * @property expectedType ์˜ˆ์ƒ๋œ ๋…ธ๋“œ ํƒ€์ž… (์„ ํƒ์‚ฌํ•ญ) + * @property actualType ์‹ค์ œ ๋…ธ๋“œ ํƒ€์ž… (์„ ํƒ์‚ฌํ•ญ) + * @property reason ์‚ฌ์œ  (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +class ASTException( + errorCode: ErrorCode, + val nodeType: String? = null, + val nodeName: String? = null, + val expectedType: String? = null, + val actualType: String? = null, + val reason: String? = null, + message: String = buildASTMessage(errorCode, nodeType, nodeName, expectedType, actualType, reason), + cause: Throwable? = null +) : DomainException(errorCode, message, cause) { + + companion object { + /** + * AST ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param errorCode ์˜ค๋ฅ˜ ์ฝ”๋“œ + * @param nodeType ๋…ธ๋“œ ํƒ€์ž… + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ + * @param expectedType ์˜ˆ์ƒ ํƒ€์ž… + * @param actualType ์‹ค์ œ ํƒ€์ž… + * @param reason ์‚ฌ์œ  + * @return ๊ตฌ์„ฑ๋œ ๋ฉ”์‹œ์ง€ + */ + private fun buildASTMessage( + errorCode: ErrorCode, + nodeType: String?, + nodeName: String?, + expectedType: String?, + actualType: String?, + reason: String? + ): String { + val baseMessage = errorCode.description + val details = mutableListOf() + + nodeType?.let { details.add("๋…ธ๋“œํƒ€์ž…: $it") } + nodeName?.let { details.add("๋…ธ๋“œ๋ช…: $it") } + expectedType?.let { details.add("์˜ˆ์ƒํƒ€์ž…: $it") } + actualType?.let { details.add("์‹ค์ œํƒ€์ž…: $it") } + reason?.let { details.add("์‚ฌ์œ : $it") } + + return if (details.isNotEmpty()) { + "$baseMessage (${details.joinToString(", ")})" + } else { + baseMessage + } + } + + /** + * AST ๊ตฌ์ถ• ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeType ๊ตฌ์ถ• ์‹คํŒจํ•œ ๋…ธ๋“œ ํƒ€์ž… + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun buildError(nodeType: String, cause: Throwable? = null): ASTException { + return ASTException( + errorCode = ErrorCode.AST_BUILD_ERROR, + nodeType = nodeType, + cause = cause + ) + } + + /** + * AST ๋…ธ๋“œ๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž… + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun notASTNode(actualType: String, nodeName: String? = null): ASTException { + return ASTException( + errorCode = ErrorCode.NOT_AST_NODE, + actualType = actualType, + nodeName = nodeName + ) + } + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” AST ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeType ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋…ธ๋“œ ํƒ€์ž… + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedASTType(nodeType: String): ASTException { + return ASTException( + errorCode = ErrorCode.UNSUPPORTED_AST_TYPE, + nodeType = nodeType + ) + } + + /** + * ์ž˜๋ชป๋œ ๋…ธ๋“œ ๊ตฌ์กฐ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeType ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ๋…ธ๋“œ ํƒ€์ž… + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun invalidNodeStructure(nodeType: String, reason: String, nodeName: String? = null): ASTException { + return ASTException( + errorCode = ErrorCode.INVALID_NODE_STRUCTURE, + nodeType = nodeType, + nodeName = nodeName, + reason = reason + ) + } + + /** + * ํƒ€์ž… ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expectedType ์˜ˆ์ƒ๋œ ํƒ€์ž… + * @param actualType ์‹ค์ œ ํƒ€์ž… + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun typeMismatch(expectedType: String, actualType: String, nodeName: String? = null): ASTException { + return ASTException( + errorCode = ErrorCode.AST_TYPE_MISMATCH, + expectedType = expectedType, + actualType = actualType, + nodeName = nodeName + ) + } + + /** + * ๋ฃจํŠธ ๋…ธ๋“œ ์œ ํšจ์„ฑ ์‹คํŒจ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param reason ์œ ํšจํ•˜์ง€ ์•Š์€ ์‚ฌ์œ  + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun invalidRootNode(reason: String): ASTException { + return ASTException( + errorCode = ErrorCode.INVALID_ROOT_NODE, + reason = reason + ) + } + + /** + * AST ํฌ๊ธฐ ์ œํ•œ ์ดˆ๊ณผ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun sizeLimitExceeded(): ASTException { + return ASTException( + errorCode = ErrorCode.AST_SIZE_EXCEEDED + ) + } + + /** + * AST ๊นŠ์ด ์ œํ•œ ์ดˆ๊ณผ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun depthLimitExceeded(): ASTException { + return ASTException( + errorCode = ErrorCode.AST_DEPTH_EXCEEDED + ) + } + + /** + * ๊ต์ฒด ๋…ธ๋“œ ์œ ํšจ์„ฑ ์‹คํŒจ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param reason ์œ ํšจํ•˜์ง€ ์•Š์€ ์‚ฌ์œ  + * @param nodeType ๋ฌธ์ œ๊ฐ€ ์žˆ๋Š” ๋…ธ๋“œ ํƒ€์ž… + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun invalidReplacementNode(reason: String, nodeType: String): ASTException { + return ASTException( + errorCode = ErrorCode.INVALID_REPLACEMENT_NODE, + reason = reason, + nodeType = nodeType + ) + } + + /** + * ์ธ์ˆ˜ ๊ฐœ์ˆ˜ ์ œํ•œ ์ดˆ๊ณผ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun argumentCountExceeded(): ASTException { + return ASTException( + errorCode = ErrorCode.AST_ARGUMENT_COUNT_EXCEEDED + ) + } + + /** + * ์ธ๋ฑ์Šค๊ฐ€ ํ—ˆ์šฉ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun indexOutOfRange(): ASTException { + return ASTException( + errorCode = ErrorCode.AST_INDEX_OUT_OF_RANGE + ) + } + + /** + * ์—ฐ์‚ฐ์ž ๊ฐ’์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun operatorEmpty(): ASTException { + return ASTException( + errorCode = ErrorCode.AST_OPERATOR_EMPTY + ) + } + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedOperator(): ASTException { + return ASTException( + errorCode = ErrorCode.AST_UNSUPPORTED_OPERATOR + ) + } + + /** + * ๊ตํ™˜๋ฒ•์น™์„ ์š”๊ตฌํ•˜๋Š” ๋ฌธ๋งฅ์—์„œ ๊ตํ™˜๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun operatorNotCommutative(operator: String): ASTException { + return ASTException( + errorCode = ErrorCode.AST_OPERATOR_NOT_COMMUTATIVE, + reason = "operator: $operator" + ) + } + + /** + * ๋ถˆ๋ฆฐ ๋ฆฌํ„ฐ๋Ÿด์ด ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun invalidBooleanValue(value: String): ASTException { + return ASTException( + errorCode = ErrorCode.INVALID_BOOLEAN_VALUE, + reason = "value: $value" + ) + } + + /** + * ํ•จ์ˆ˜๋ช…์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionNameEmpty(): ASTException { + return ASTException( + errorCode = ErrorCode.AST_FUNCTION_NAME_EMPTY + ) + } + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ ํ•จ์ˆ˜๋ช…์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun invalidFunctionName(name: String): ASTException { + return ASTException( + errorCode = ErrorCode.AST_INVALID_FUNCTION_NAME, + reason = "function name: $name" + ) + } + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun argumentsEmpty(): ASTException { + return ASTException( + errorCode = ErrorCode.AST_ARGUMENTS_EMPTY + ) + } + + /** + * IF ๋…ธ๋“œ๋ฅผ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun ifNotSimplifiable( + nodeName: String? = "IfNode" + ): ASTException { + return ASTException( + errorCode = ErrorCode.AST_IF_NOT_SIMPLIFIABLE, + nodeType = "IfNode", + nodeName = nodeName, + ) + } + + /** + * ๋‹จ์ˆœํ™” ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌํ•˜์ง€ ๋ชปํ•œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์ผ€์ด์Šค๊ฐ€ ๋ฐœ์ƒํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun simplificationUnexpectedCase( + nodeName: String? = "IfNode" + ): ASTException { + return ASTException( + errorCode = ErrorCode.AST_SIMPLIFICATION_UNEXPECTED_CASE, + nodeType = "IfNode", + nodeName = nodeName, + ) + } + + /** + * ์ˆซ์ž ๊ฐ’์ด ์œ ํ•œํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๊ฒ€์‚ฌํ•œ ์›๋ณธ ๊ฐ’ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun numberNotFinite(value: Double): ASTException = + ASTException( + errorCode = ErrorCode.AST_NON_FINITE_NUMBER, + nodeType = "NumberNode", + reason = "value: $value" + ) + + /** + * ์ •์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฐ’์„ Int๋กœ ๋ณ€ํ™˜ํ•˜๋ ค ํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€ํ™˜ ๋Œ€์ƒ ๊ฐ’ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun notIntegerForInt(value: Double): ASTException = + ASTException( + errorCode = ErrorCode.AST_NON_INTEGER_TO_INT, + nodeType = "NumberNode", + reason = "value: $value" + ) + + /** + * ์ •์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฐ’์„ Long์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋ ค ํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€ํ™˜ ๋Œ€์ƒ ๊ฐ’ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun notIntegerForLong(value: Double): ASTException = + ASTException( + errorCode = ErrorCode.AST_NON_INTEGER_TO_LONG, + nodeType = "NumberNode", + reason = "value: $value" + ) + + /** + * 0์œผ๋กœ ๋‚˜๋ˆ„๋ ค ํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun divisionByZero(): ASTException = + ASTException( + errorCode = ErrorCode.AST_DIVISION_BY_ZERO, + nodeType = "NumberNode", + reason = "denominator=0" + ) + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์ „๋‹ฌ๋œ ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด(์˜ˆ: "!", "-") + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ ๋˜๋Š” ์‹๋ณ„์ž(์„ ํƒ) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedUnaryOperator( + operator: String, + nodeName: String? = null + ): ASTException { + return ASTException( + errorCode = ErrorCode.AST_UNSUPPORTED_UNARY_OPERATOR, + nodeType = "UnaryOpNode", + nodeName = nodeName, + reason = "operator: $operator" + ) + } + + /** + * ์ด์ค‘ ์Œ์ˆ˜(์˜ˆ: `--x`)๋ฅผ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * ์ ์šฉ ์˜ค๋ฅ˜ ์ฝ”๋“œ: [ErrorCode.AST_DOUBLE_NEGATION_NOT_SIMPLIFIABLE] + * + * @param detail ๋ถˆ๊ฐ€ ์‚ฌ์œ  ์ƒ์„ธ(์„ ํƒ) + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ ๋˜๋Š” ์‹๋ณ„์ž(๊ธฐ๋ณธ๊ฐ’: `"UnaryOpNode"`) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun doubleNegationNotSimplifiable( + detail: String? = null, + nodeName: String? = "UnaryOpNode" + ): ASTException { + return ASTException( + errorCode = ErrorCode.AST_DOUBLE_NEGATION_NOT_SIMPLIFIABLE, + nodeType = "UnaryOpNode", + nodeName = nodeName, + reason = detail + ) + } + + /** + * ์ด์ค‘ ๋…ผ๋ฆฌ ๋ถ€์ •(์˜ˆ: `!!x`)์„ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param detail ๋ถˆ๊ฐ€ ์‚ฌ์œ  ์ƒ์„ธ(์„ ํƒ) + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ ๋˜๋Š” ์‹๋ณ„์ž(๊ธฐ๋ณธ๊ฐ’: `"UnaryOpNode"`) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun doubleLogicalNegationNotSimplifiable( + detail: String? = null, + nodeName: String? = "UnaryOpNode" + ): ASTException { + return ASTException( + errorCode = ErrorCode.AST_DOUBLE_LOGICAL_NEGATION_NOT_SIMPLIFIABLE, + nodeType = "UnaryOpNode", + nodeName = nodeName, + reason = detail + ) + } + + /** + * ๋ณ€์ˆ˜๋ช…์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeType ๋…ธ๋“œ ํƒ€์ž…(๊ธฐ๋ณธ: "VariableNode") + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ ๋˜๋Š” ์‹๋ณ„์ž(์„ ํƒ) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun variableNameEmpty( + nodeType: String? = "VariableNode", + nodeName: String? = null + ): ASTException = ASTException( + errorCode = ErrorCode.AST_VARIABLE_NAME_EMPTY, + nodeType = nodeType, + nodeName = nodeName + ) + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช…์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๊ฒ€์ฆ ์‹คํŒจํ•œ ๋ณ€์ˆ˜๋ช… + * @param nodeType ๋…ธ๋“œ ํƒ€์ž…(๊ธฐ๋ณธ: "VariableNode") + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ ๋˜๋Š” ์‹๋ณ„์ž(์„ ํƒ) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun invalidVariableName( + name: String, + nodeType: String? = "VariableNode", + nodeName: String? = null + ): ASTException = ASTException( + errorCode = ErrorCode.AST_INVALID_VARIABLE_NAME, + nodeType = nodeType, + nodeName = nodeName, + reason = "name: $name" + ) + + /** + * ๋ณ€์ˆ˜ ํ‘œ๊ธฐ ๋ฌธ์ž์—ด์ด ์ค‘๊ด„ํ˜ธ๋กœ ๋‘˜๋Ÿฌ์‹ธ์—ฌ ์žˆ์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์›๋ณธ ๋ฌธ์ž์—ด(์˜ˆ: "{USER_NAME}") + * @param expectedOpen ๊ธฐ๋Œ€ ์—ฌ๋Š” ๊ด„ํ˜ธ(๊ธฐ๋ณธ: "{") + * @param expectedClose ๊ธฐ๋Œ€ ๋‹ซ๋Š” ๊ด„ํ˜ธ(๊ธฐ๋ณธ: "}") + * @param nodeType ๋…ธ๋“œ ํƒ€์ž…(๊ธฐ๋ณธ: "VariableNode") + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„ ๋˜๋Š” ์‹๋ณ„์ž(์„ ํƒ) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun variableNotBracketed( + value: String, + expectedOpen: String = "{", + expectedClose: String = "}", + nodeType: String? = "VariableNode", + nodeName: String? = null + ): ASTException = ASTException( + errorCode = ErrorCode.AST_VARIABLE_NOT_BRACKETED, + nodeType = nodeType, + nodeName = nodeName, + reason = "value: $value, expected: $expectedOpen...$expectedClose" + ) + + /** + * ๋…ธ๋“œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeType ๋…ธ๋“œ ํƒ€์ž… + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„/์‹๋ณ„์ž(์„ ํƒ) + * @param reason ์‹คํŒจ ์‚ฌ์œ (์„ ํƒ) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun nodeValidationFailed( + nodeName: String? = null, + reason: String? = null + ): ASTException = ASTException( + errorCode = ErrorCode.AST_VALIDATION_FAILED, + nodeName = nodeName, + reason = reason + ) + + /** + * ๋…ธ๋“œ ๊ตฌ์กฐ ๊ฒ€์ฆ ์‹คํŒจ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeType ๋…ธ๋“œ ํƒ€์ž… + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„/์‹๋ณ„์ž(์„ ํƒ) + * @param reason ์‹คํŒจ ์‚ฌ์œ (์„ ํƒ) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun nodeStructureFailed( + nodeName: String? = null, + reason: String? = null + ): ASTException = ASTException( + errorCode = ErrorCode.INVALID_NODE_STRUCTURE, + nodeName = nodeName, + reason = reason + ) + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์›๋ณธ ๋ฌธ์ž์—ด + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun invalidNumberLiteral(value: String): ASTException = + ASTException( + errorCode = ErrorCode.AST_INVALID_NUMBER_LITERAL, + nodeType = "NumberNode", + reason = "value=$value" + ) + + /** + * ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์ „๋‹ฌ๋œ ์—ฐ์‚ฐ์ž + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun notArithmeticOperator(operator: String): ASTException = + ASTException( + errorCode = ErrorCode.AST_NOT_ARITHMETIC_OPERATOR, + nodeType = "BinaryOpNode", + reason = "operator=$operator" + ) + + /** + * ๋น„๊ต ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์ „๋‹ฌ๋œ ์—ฐ์‚ฐ์ž + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun notComparisonOperator(operator: String): ASTException = + ASTException( + errorCode = ErrorCode.AST_NOT_COMPARISON_OPERATOR, + nodeType = "BinaryOpNode", + reason = "operator=$operator" + ) + + /** + * ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์ „๋‹ฌ๋œ ์—ฐ์‚ฐ์ž + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun notLogicalOperator(operator: String): ASTException = + ASTException( + errorCode = ErrorCode.AST_NOT_LOGICAL_OPERATOR, + nodeType = "BinaryOpNode", + reason = "operator=$operator" + ) + + /** + * ์ง€์›๋˜์ง€ ์•Š๋Š” ์ˆ˜ํ•™ ํ•จ์ˆ˜์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedMathFunction(name: String): ASTException = + ASTException( + errorCode = ErrorCode.AST_UNSUPPORTED_MATH_FUNCTION, + nodeType = "FunctionCallNode", + reason = "name=$name" + ) + + /** + * ArgsMultiple ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ ์ž์‹ ๊ฐœ์ˆ˜(๊ธฐ๋ณธ: 3) + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„/์‹๋ณ„์ž(์„ ํƒ) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun argsMultipleChildrenMismatch( + expected: Int = 3, + actual: Int, + nodeName: String? = null + ): ASTException = ASTException( + errorCode = ErrorCode.AST_ARGS_MULTIPLE_CHILDREN_MISMATCH, + nodeName = nodeName, + reason = "expected: $expected, actual: $actual" + ) + + /** + * ArgsSingle ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ ์ž์‹ ๊ฐœ์ˆ˜(๊ธฐ๋ณธ: 1) + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + * @param nodeName ๋…ธ๋“œ ์ด๋ฆ„/์‹๋ณ„์ž(์„ ํƒ) + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun argsSingleChildMismatch( + expected: Int = 1, + actual: Int, + nodeName: String? = null + ): ASTException = ASTException( + errorCode = ErrorCode.AST_ARGS_SINGLE_CHILD_MISMATCH, + nodeName = nodeName, + reason = "expected: $expected, actual: $actual" + ) + + /** + * BinaryOp ๋นŒ๋”์˜ ์ž์‹ ์ˆ˜๊ฐ€ ์š”๊ตฌ์‚ฌํ•ญ์— ๋ชป ๋ฏธ์น  ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param required ํ•„์š”ํ•œ ์ตœ์†Œ ์ž์‹ ์ˆ˜ + * @param actual ์‹ค์ œ ์ž์‹ ์ˆ˜ + * @param leftIndex ์™ผ์ชฝ ํ”ผ์—ฐ์‚ฐ์ž ์ธ๋ฑ์Šค + * @param rightIndex ์˜ค๋ฅธ์ชฝ ํ”ผ์—ฐ์‚ฐ์ž ์ธ๋ฑ์Šค + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun binaryChildrenInsufficient( + required: Int, + actual: Int, + leftIndex: Int, + rightIndex: Int + ): ASTException = ASTException( + errorCode = ErrorCode.AST_BINARY_CHILDREN_INSUFFICIENT, + reason = "required: $required(actual indices need 0..${required-1}), " + + "actual: $actual, leftIndex: $leftIndex, rightIndex: $rightIndex" + ) + + /** + * BinaryOp ๋นŒ๋”์˜ ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ AST ๋…ธ๋“œ๊ฐ€ ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param side "left" ๋˜๋Š” "right" + * @param actualType ๋Ÿฐํƒ€์ž„ ํƒ€์ž…๋ช… + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun operandNotAst( + side: String, + actualType: String? + ): ASTException = ASTException( + errorCode = ErrorCode.NOT_AST_NODE, // AST002 ์žฌ์‚ฌ์šฉ + reason = "operand: $side, actualType: $actualType" + ) + + /** + * FunctionCall ๋นŒ๋”์˜ ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ํ•˜๋Š” ์ž์‹ ๊ฐœ์ˆ˜ (๊ธฐ๋ณธ: 3) + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionCallChildrenMismatch( + expected: Int = 3, + actual: Int, + ): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_CALL_CHILDREN_MISMATCH, + reason = "expected: $expected, actual: $actual" + ) + + /** + * FunctionCall ๋นŒ๋”์˜ ์ฒซ ๋ฒˆ์งธ ์ž์‹์ด Token์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž… ์ด๋ฆ„ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionCallFirstNotToken(actualType: String?): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_CALL_FIRST_NOT_TOKEN, + reason = "actualType: $actualType" + ) + + /** + * FunctionCall ๋นŒ๋”์˜ ์„ธ ๋ฒˆ์งธ ์ž์‹์ด List๊ฐ€ ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž… ์ด๋ฆ„ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionCallThirdNotList(actualType: String?): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_CALL_THIRD_NOT_LIST, + reason = "actualType: $actualType" + ) + + /** + * FunctionCall ๋นŒ๋”์˜ ์ธ์ˆ˜ ๋ชฉ๋ก์— ASTNode๊ฐ€ ์•„๋‹Œ ์š”์†Œ๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionCallArgsNotAstNode(): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_CALL_ARGS_NOT_AST_NODE, + ) + + /** + * FunctionCallEmpty ๋นŒ๋”์˜ ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฅผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ํ•˜๋Š” ์ž์‹ ๊ฐœ์ˆ˜ (๊ธฐ๋ณธ: 3) + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionCallEmptyChildrenMismatch( + expected: Int = 3, + actual: Int + ): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_CALL_EMPTY_CHILDREN_MISMATCH, + reason = "expected: $expected, actual: $actual" + ) + + /** + * FunctionCallEmpty ๋นŒ๋”์˜ ์ฒซ ๋ฒˆ์งธ ์ž์‹์ด Token์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž… ์ด๋ฆ„ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionCallEmptyFirstNotToken(actualType: String?): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_CALL_EMPTY_FIRST_NOT_TOKEN, + reason = "actualType: $actualType" + ) + + /** + * FunctionCallEmpty ๋นŒ๋”์˜ ๋‘ ๋ฒˆ์งธ ์ž์‹์ด Token์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž… ์ด๋ฆ„ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionCallEmptySecondNotToken(actualType: String?): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_CALL_EMPTY_SECOND_NOT_TOKEN, + reason = "actualType: $actualType" + ) + + /** + * FunctionCallEmpty ๋นŒ๋”์˜ ์„ธ ๋ฒˆ์งธ ์ž์‹์ด Token์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž… ์ด๋ฆ„ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionCallEmptyThirdNotToken(actualType: String?): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_CALL_EMPTY_THIRD_NOT_TOKEN, + reason = "actualType: $actualType" + ) + + /** + * Identity ๋นŒ๋”์˜ ์ž์‹ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expectedAtLeast ์ตœ์†Œ ํ•„์š” ๊ฐœ์ˆ˜(๊ธฐ๋ณธ: 1) + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun identityChildrenEmpty( + expectedAtLeast: Int = 1, + actual: Int + ): ASTException = ASTException( + errorCode = ErrorCode.AST_IDENTITY_CHILDREN_EMPTY, + reason = "expectedโ‰ฅ$expectedAtLeast, actual=$actual" + ) + + /** + * Identity ๋นŒ๋”์˜ ์ฒซ ๋ฒˆ์งธ ์ž์‹์ด ASTNode๊ฐ€ ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž… ์ด๋ฆ„ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun identityFirstNotAstNode(actualType: String?): ASTException = ASTException( + errorCode = ErrorCode.NOT_AST_NODE, // ์žฌ์‚ฌ์šฉ + reason = "child=first, actualType=$actualType" + ) + + /** + * If ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ ์ž์‹ ๊ฐœ์ˆ˜ + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + */ + fun ifChildrenMismatch(expected: Int, actual: Int): ASTException = ASTException( + errorCode = ErrorCode.AST_IF_CHILDREN_MISMATCH, + reason = "expected: $expected, actual: $actual" + ) + + /** + * Number ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ ์ž์‹ ๊ฐœ์ˆ˜ + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + */ + fun numberChildrenMismatch(expected: Int, actual: Int): ASTException = ASTException( + errorCode = ErrorCode.AST_NUMBER_CHILDREN_MISMATCH, + reason = "expected: $expected, actual: $actual" + ) + + /** + * Parenthesized ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ ์ž์‹ ๊ฐœ์ˆ˜ + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + */ + fun parenthesizedChildrenMismatch(expected: Int, actual: Int): ASTException = ASTException( + errorCode = ErrorCode.AST_PARENTHESIZED_CHILDREN_MISMATCH, + reason = "expected: $expected, actual: $actual" + ) + + /** + * Parenthesized ๋นŒ๋” ๋‘ ๋ฒˆ์งธ ์ž์‹ ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž…๋ช… + */ + fun parenthesizedSecondNotAst(actualType: String?): ASTException = ASTException( + errorCode = ErrorCode.AST_PARENTHESIZED_SECOND_NOT_AST, + reason = "actualType: $actualType" + ) + + /** + * Start ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ ์ž์‹ ๊ฐœ์ˆ˜ + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun startChildrenMismatch(expected: Int, actual: Int): ASTException = + ASTException( + errorCode = ErrorCode.AST_START_CHILDREN_MISMATCH, + reason = "expected: $expected, actual: $actual" + ) + + /** + * Start ๋นŒ๋” ์ฒซ ๋ฒˆ์งธ ์ž์‹ ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž…๋ช… + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun startFirstNotAst(actualType: String?): ASTException = + ASTException( + errorCode = ErrorCode.NOT_AST_NODE, // ์žฌ์‚ฌ์šฉ + reason = "child=first, actualType: $actualType" + ) + + /** + * UnaryOp ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜ ๋ถ€์กฑ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param required ์ตœ์†Œ ํ•„์š” ์ž์‹ ๊ฐœ์ˆ˜ + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + * @param operandIndex ํ”ผ์—ฐ์‚ฐ์ž ์ธ๋ฑ์Šค + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun unaryChildrenInsufficient( + required: Int, + actual: Int, + operandIndex: Int + ): ASTException = ASTException( + errorCode = ErrorCode.AST_UNARY_CHILDREN_INSUFFICIENT, + reason = "required: $required, actual: $actual, operandIndex: $operandIndex" + ) + + /** + * Variable ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜ ๋ถˆ์ผ์น˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ ์ž์‹ ๊ฐœ์ˆ˜ + * @param actual ์‹ค์ œ ์ž์‹ ๊ฐœ์ˆ˜ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun variableChildrenMismatch(expected: Int, actual: Int): ASTException = + ASTException( + errorCode = ErrorCode.AST_VARIABLE_CHILDREN_MISMATCH, + reason = "expected: $expected, actual: $actual" + ) + + /** + * Variable ๋นŒ๋” ์ฒซ ๋ฒˆ์งธ ์ž์‹ ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํƒ€์ž…๋ช… + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun variableFirstNotToken(actualType: String?): ASTException = + ASTException( + errorCode = ErrorCode.AST_VARIABLE_FIRST_NOT_TOKEN, + reason = "actualType: $actualType" + ) + + /** + * ์ˆซ์ž ๊ฐ’์ด NaN์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value NaN์ธ ์ˆซ์ž ๊ฐ’ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun numberIsNaN(value: Double): ASTException = ASTException( + errorCode = ErrorCode.AST_NUMBER_IS_NAN, + reason = "value=$value" + ) + + /** + * ์ˆซ์ž ๊ฐ’์ด ์ตœ์†Œ๊ฐ’ ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์‹ค์ œ ์ˆซ์ž ๊ฐ’ + * @param min ์ตœ์†Œ ํ—ˆ์šฉ๊ฐ’ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun numberTooSmall(value: Double, min: Double): ASTException = ASTException( + errorCode = ErrorCode.AST_NUMBER_TOO_SMALL, + reason = "value=$value, min=$min" + ) + + /** + * ์ˆซ์ž ๊ฐ’์ด ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์‹ค์ œ ์ˆซ์ž ๊ฐ’ + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ๊ฐ’ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun numberTooLarge(value: Double, max: Double): ASTException = ASTException( + errorCode = ErrorCode.AST_NUMBER_TOO_LARGE, + reason = "value=$value, max=$max" + ) + + /** + * ๋ณ€์ˆ˜๋ช…์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param length ์‹ค์ œ ๋ณ€์ˆ˜๋ช… ๊ธธ์ด + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ธธ์ด + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun variableNameTooLong(length: Int, max: Int): ASTException = ASTException( + errorCode = ErrorCode.AST_VARIABLE_NAME_TOO_LONG, + reason = "length=$length, max=$max" + ) + + /** + * ์˜ˆ์•ฝ์–ด๋ฅผ ๋ณ€์ˆ˜๋ช…์œผ๋กœ ์‚ฌ์šฉํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์˜ˆ์•ฝ์–ด ๋ณ€์ˆ˜๋ช… + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun variableReservedWord(name: String): ASTException = ASTException( + errorCode = ErrorCode.AST_VARIABLE_RESERVED_WORD, + reason = "name=$name" + ) + + /** + * ์ง€์›๋˜์ง€ ์•Š๋Š” ์ดํ•ญ ์—ฐ์‚ฐ์ž์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๊ธฐํ˜ธ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedBinaryOperator(operator: String): ASTException = ASTException( + errorCode = ErrorCode.AST_UNSUPPORTED_BINARY_OPERATOR, + reason = "operator=$operator" + ) + + /** + * 0์œผ๋กœ ๋‚˜๋จธ์ง€ ์—ฐ์‚ฐ์„ ์‹œ๋„ํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun moduloByZero(): ASTException = ASTException( + errorCode = ErrorCode.AST_MODULO_BY_ZERO, + ) + + /** + * 0^0 ์—ฐ์‚ฐ์„ ์‹œ๋„ํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun zeroPowerZero(): ASTException = ASTException( + errorCode = ErrorCode.AST_ZERO_POWER_ZERO_UNDEFINED, + ) + + /** + * ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž์˜ ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ๋…ผ๋ฆฌ์ ์œผ๋กœ ํ˜ธํ™˜๋˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun logicalIncompatibleOperand(): ASTException = ASTException( + errorCode = ErrorCode.AST_LOGICAL_INCOMPATIBLE_OPERAND, + ) + + /** + * ํ•จ์ˆ˜๋ช…์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param length ์‹ค์ œ ํ•จ์ˆ˜๋ช… ๊ธธ์ด + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ธธ์ด + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionNameTooLong(length: Int, max: Int): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_NAME_TOO_LONG, + reason = "length=$length, max=$max" + ) + + /** + * ํ•จ์ˆ˜ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param size ์‹ค์ œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฐœ์ˆ˜ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionArgumentsExceeded(size: Int, max: Int): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_ARGUMENTS_EXCEEDED, + reason = "args=$size, max=$max" + ) + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @param expectedDesc ์š”๊ตฌ๋˜๋Š” ์ธ์ˆ˜ ๊ฐœ์ˆ˜ ์„ค๋ช… + * @param actual ์‹ค์ œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun functionArgumentCountMismatch( + name: String, + expectedDesc: String, + actual: Int + ): ASTException = ASTException( + errorCode = ErrorCode.AST_FUNCTION_ARGUMENT_COUNT_MISMATCH, + reason = "name=$name, expected=$expectedDesc, actual=$actual" + ) + + /** + * ์กฐ๊ฑด๋ฌธ์˜ ์ด ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ ๊นŠ์ด๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param totalDepth ์‹ค์ œ ์ด ๊นŠ์ด + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊นŠ์ด + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun ifTotalDepthExceeded(totalDepth: Int, max: Int): ASTException = ASTException( + errorCode = ErrorCode.AST_IF_TOTAL_DEPTH_EXCEEDED, + reason = "totalDepth=$totalDepth, max=$max" + ) + + /** + * ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param size ์‹ค์ œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฐœ์ˆ˜ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun argumentsExceeded(size: Int, max: Int): ASTException = ASTException( + errorCode = ErrorCode.AST_ARGUMENTS_EXCEEDED, + reason = "args=$size, max=$max" + ) + + /** + * ์ค‘๋ณต๋œ ์ธ์ˆ˜๊ฐ€ ์กด์žฌํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param duplicates ์ค‘๋ณต๋œ ์ธ์ˆ˜ ๋ชฉ๋ก + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun argumentsDuplicated(duplicates: Collection<*>): ASTException = ASTException( + errorCode = ErrorCode.AST_ARGUMENTS_DUPLICATED, + reason = "duplicates=$duplicates" + ) + + /** + * ๋…ธ๋“œ ํฌ๊ธฐ๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ ํฌ๊ธฐ๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param size ์‹ค์ œ ํฌ๊ธฐ + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ํฌ๊ธฐ + * @param context ์ปจํ…์ŠคํŠธ ๋ผ๋ฒจ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun nodeSizeExceeded(size: Int, max: Int, context: String): ASTException = ASTException( + errorCode = ErrorCode.AST_NODE_SIZE_EXCEEDED, + reason = "$context size=$size, max=$max" + ) + + /** + * ๋…ธ๋“œ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ ๊นŠ์ด๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param depth ์‹ค์ œ ๊นŠ์ด + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊นŠ์ด + * @param context ์ปจํ…์ŠคํŠธ ๋ผ๋ฒจ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun nodeDepthExceeded(depth: Int, max: Int, context: String): ASTException = ASTException( + errorCode = ErrorCode.AST_NODE_DEPTH_EXCEEDED, + reason = "$context depth=$depth, max=$max" + ) + + /** + * ๋…ธ๋“œ์˜ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param count ์‹ค์ œ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฐœ์ˆ˜ + * @param context ์ปจํ…์ŠคํŠธ ๋ผ๋ฒจ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun nodeVariablesExceeded(count: Int, max: Int, context: String): ASTException = ASTException( + errorCode = ErrorCode.AST_NODE_VARIABLES_EXCEEDED, + reason = "$context variables=$count, max=$max" + ) + + /** + * ํŠธ๋ฆฌ ๊นŠ์ด๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ๊นŠ์ด ๊ฐ’ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun treeDepthNegative(actual: Int): ASTException = + ASTException( + errorCode = ErrorCode.AST_TREE_DEPTH_NEGATIVE, + nodeType = "Tree", + reason = "actual=$actual" + ) + + /** + * ํŠธ๋ฆฌ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ๊ฐ’์„ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ๊นŠ์ด ๊ฐ’ + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊นŠ์ด + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun treeDepthTooLarge(actual: Int, max: Int): ASTException = + ASTException( + errorCode = ErrorCode.AST_TREE_DEPTH_TOO_LARGE, + nodeType = "Tree", + reason = "actual=$actual, max=$max" + ) + + /** + * ํ˜„์žฌ ๋ฒ„์ „์—์„œ ๋Ÿฐํƒ€์ž„ ๊ทœ์น™ ์ถ”๊ฐ€๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun runtimeRuleNotSupported(): ASTException = + ASTException( + errorCode = ErrorCode.AST_RUNTIME_RULE_NOT_SUPPORTED + ) + + /** + * ๋…ธ๋“œ ํฌ๊ธฐ๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ํฌ๊ธฐ ๊ฐ’ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun nodeSizeNegative(actual: Int): ASTException = + ASTException( + errorCode = ErrorCode.AST_NODE_SIZE_NEGATIVE, + reason = "actual=$actual" + ) + + /** + * ๋…ธ๋“œ ํฌ๊ธฐ๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ๊ฐ’์„ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ํฌ๊ธฐ ๊ฐ’ + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ํฌ๊ธฐ + * @return ASTException ์ธ์Šคํ„ด์Šค + */ + fun nodeSizeTooLarge(actual: Int, max: Int): ASTException = + ASTException( + errorCode = ErrorCode.AST_NODE_SIZE_TOO_LARGE, + reason = "actual=$actual, max=$max" + ) + + } + + /** + * AST ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋…ธ๋“œ ํƒ€์ž…, ์ด๋ฆ„, ์˜ˆ์ƒ/์‹ค์ œ ํƒ€์ž… ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋งต + */ + fun getASTInfo(): Map = mapOf( + "nodeType" to nodeType, + "nodeName" to nodeName, + "expectedType" to expectedType, + "actualType" to actualType + ).filterValues { it != null } + + /** + * ์ „์ฒด ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ์ •๋ณด์™€ AST ์ •๋ณด๊ฐ€ ๊ฒฐํ•ฉ๋œ ๋งต + */ + fun getFullErrorInfo(): Map { + val baseInfo = super.toErrorInfo().toMutableMap() + val astInfo = getASTInfo() + + astInfo.forEach { (key, value) -> + baseInfo[key] = value.toString() + } + + return baseInfo + } + + override fun toString(): String { + val astDetails = getASTInfo() + return if (astDetails.isNotEmpty()) { + "${super.toString()}, ast=${astDetails}" + } else { + super.toString() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factories/ASTNodeFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factories/ASTNodeFactory.kt new file mode 100644 index 00000000..c78bd590 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factories/ASTNodeFactory.kt @@ -0,0 +1,479 @@ +package hs.kr.entrydsm.domain.ast.factories + +import hs.kr.entrydsm.domain.ast.entities.* +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.specifications.ASTValiditySpec +import hs.kr.entrydsm.domain.ast.specifications.NodeStructureSpec +import hs.kr.entrydsm.domain.ast.policies.ASTValidationPolicy +import hs.kr.entrydsm.domain.ast.policies.NodeCreationPolicy +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import java.util.concurrent.atomic.AtomicLong + +/** + * AST ๋…ธ๋“œ ๊ฐ์ฒด๋“ค์„ ์ƒ์„ฑํ•˜๋Š” ํŒฉํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. + * + * ๋‹ค์–‘ํ•œ ํƒ€์ž…์˜ AST ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋ฉฐ, ๋„๋ฉ”์ธ ๊ทœ์น™๊ณผ ์ •์ฑ…์„ + * ์ ์šฉํ•˜์—ฌ ์ผ๊ด€๋œ ๊ฐ์ฒด ์ƒ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Factory(context = "ast", complexity = Complexity.HIGH, cache = true) +class ASTNodeFactory { + + private val validitySpec = ASTValiditySpec() + private val structureSpec = NodeStructureSpec() + private val validationPolicy = ASTValidationPolicy() + private val creationPolicy = NodeCreationPolicy() + + companion object { + + private val ARITHMETIC_OPERATORS = setOf( + "+", "-", "*", "/", "^", "%" + ) + + private val COMPARISON_OPERATORS = setOf( + "==", "!=", "<", "<=", ">", ">=" + ) + + private val LOGICAL_OPERATORS = setOf( + "&&", "||" + ) + + private const val PLUS = "+" + private const val EXCLAMATION = "!" + private const val MINUS = "-" + + /** + * ์ง€์›๋˜๋Š” ์ˆ˜ํ•™ ํ•จ์ˆ˜ ๋ชฉ๋ก์ž…๋‹ˆ๋‹ค. + */ + private val SUPPORTED_MATH_FUNCTIONS = setOf( + "SIN", "COS", "TAN", "SQRT", "ABS", "LOG", "EXP" + ) + + private val createdNodeCount = AtomicLong(0) + private val createdNumberCount = AtomicLong(0) + private val createdBooleanCount = AtomicLong(0) + private val createdVariableCount = AtomicLong(0) + private val createdBinaryOpCount = AtomicLong(0) + private val createdUnaryOpCount = AtomicLong(0) + private val createdFunctionCallCount = AtomicLong(0) + private val createdIfCount = AtomicLong(0) + private val createdArgumentsCount = AtomicLong(0) + + private val instance = ASTNodeFactory() + + /** + * ์‹ฑ๊ธ€ํ†ค ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + @JvmStatic + fun getInstance(): ASTNodeFactory = instance + + /** + * ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํŽธ์˜ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + */ + @JvmStatic + fun quickCreateNumber(value: Double): NumberNode { + return getInstance().createNumber(value) + } + + @JvmStatic + fun quickCreateBoolean(value: Boolean): BooleanNode { + return getInstance().createBoolean(value) + } + + @JvmStatic + fun quickCreateVariable(name: String): VariableNode { + return getInstance().createVariable(name) + } + } + + /** + * ์ˆซ์ž ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ˆซ์ž ๊ฐ’ + * @return NumberNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฐ’์ธ ๊ฒฝ์šฐ + */ + fun createNumber(value: Double): NumberNode { + // ์ƒ์„ฑ ์ „ ์ •์ฑ… ๊ฒ€์ฆ + creationPolicy.validateNumberCreation(value) + + val node = NumberNode(value) + + // ์ƒ์„ฑ ํ›„ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + if (!validitySpec.isSatisfiedBy(node)) { + throw ASTException.nodeValidationFailed( + reason = validitySpec.getWhyNotSatisfied(node) + ) + } + + createdNumberCount.incrementAndGet() + + return node + } + + /** + * ๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ถˆ๋ฆฌ์–ธ ๊ฐ’ + * @return BooleanNode ์ธ์Šคํ„ด์Šค + */ + fun createBoolean(value: Boolean): BooleanNode { + // ์ƒ์„ฑ ์ „ ์ •์ฑ… ๊ฒ€์ฆ + creationPolicy.validateBooleanCreation(value) + + val node = BooleanNode(value) + + // ์ƒ์„ฑ ํ›„ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + // ์ˆซ์ž/๋ถˆ๋ฆฌ์–ธ/๋ณ€์ˆ˜/์—ฐ์‚ฐ/ํ•จ์ˆ˜ ํ˜ธ์ถœ/์กฐ๊ฑด๋ฌธ/์ธ์ˆ˜ ๋ชฉ๋ก ๋…ธ๋“œ ์ƒ์„ฑ ํ›„ ์œ ํšจ์„ฑ + if (!validitySpec.isSatisfiedBy(node)) { + throw ASTException.nodeValidationFailed( + reason = validitySpec.getWhyNotSatisfied(node) + ) + } + + createdBooleanCount.incrementAndGet() + + return node + } + + /** + * ๋ณ€์ˆ˜ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜๋ช… + * @return VariableNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช…์ธ ๊ฒฝ์šฐ + */ + fun createVariable(name: String): VariableNode { + // ์ƒ์„ฑ ์ „ ์ •์ฑ… ๊ฒ€์ฆ + creationPolicy.validateVariableCreation(name) + + val node = VariableNode(name) + + // ์ƒ์„ฑ ํ›„ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + if (!validitySpec.isSatisfiedBy(node)) { + throw ASTException.nodeValidationFailed( + reason = validitySpec.getWhyNotSatisfied(node) + ) + } + + createdVariableCount.incrementAndGet() + + return node + } + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @param operator ์—ฐ์‚ฐ์ž + * @param right ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @return BinaryOpNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ์—ฐ์‚ฐ์ž์ด๊ฑฐ๋‚˜ ํ”ผ์—ฐ์‚ฐ์ž์ธ ๊ฒฝ์šฐ + */ + fun createBinaryOp(left: ASTNode, operator: String, right: ASTNode): BinaryOpNode { + // ์ƒ์„ฑ ์ „ ์ •์ฑ… ๊ฒ€์ฆ + creationPolicy.validateBinaryOpCreation(left, operator, right) + + val node = BinaryOpNode(left, operator, right) + validateNodeAfterBuild(node) + + createdBinaryOpCount.incrementAndGet() + + return node + } + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž + * @param operand ํ”ผ์—ฐ์‚ฐ์ž + * @return UnaryOpNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ์—ฐ์‚ฐ์ž์ด๊ฑฐ๋‚˜ ํ”ผ์—ฐ์‚ฐ์ž์ธ ๊ฒฝ์šฐ + */ + fun createUnaryOp(operator: String, operand: ASTNode): UnaryOpNode { + // ์ƒ์„ฑ ์ „ ์ •์ฑ… ๊ฒ€์ฆ + creationPolicy.validateUnaryOpCreation(operator, operand) + + val node = UnaryOpNode(operator, operand) + validateNodeAfterBuild(node) + + createdUnaryOpCount.incrementAndGet() + + return node + } + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @param args ์ธ์ˆ˜ ๋ชฉ๋ก + * @return FunctionCallNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ํ•จ์ˆ˜๋ช…์ด๊ฑฐ๋‚˜ ์ธ์ˆ˜์ธ ๊ฒฝ์šฐ + */ + fun createFunctionCall(name: String, args: List): FunctionCallNode { + // ์ƒ์„ฑ ์ „ ์ •์ฑ… ๊ฒ€์ฆ + creationPolicy.validateFunctionCallCreation(name, args) + + val node = FunctionCallNode(name, args) + validateNodeAfterBuild(node) + + createdFunctionCallCount.incrementAndGet() + + return node + } + + /** + * ์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param condition ์กฐ๊ฑด์‹ + * @param trueValue ์ฐธ ๊ฐ’ + * @param falseValue ๊ฑฐ์ง“ ๊ฐ’ + * @return IfNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ์กฐ๊ฑด์ด๊ฑฐ๋‚˜ ๊ฐ’์ธ ๊ฒฝ์šฐ + */ + fun createIf(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode): IfNode { + // ์ƒ์„ฑ ์ „ ์ •์ฑ… ๊ฒ€์ฆ + creationPolicy.validateIfCreation(condition, trueValue, falseValue) + + val node = IfNode(condition, trueValue, falseValue) + validateNodeAfterBuild(node) + + createdIfCount.incrementAndGet() + + return node + } + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param arguments ์ธ์ˆ˜ ๋ชฉ๋ก + * @return ArgumentsNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ์ธ์ˆ˜์ธ ๊ฒฝ์šฐ + */ + fun createArguments(arguments: List): ArgumentsNode { + // ์ƒ์„ฑ ์ „ ์ •์ฑ… ๊ฒ€์ฆ + creationPolicy.validateArgumentsCreation(arguments) + + val node = ArgumentsNode(arguments) + + // ์ƒ์„ฑ ํ›„ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + if (!validitySpec.isSatisfiedBy(node)) { + throw ASTException.nodeValidationFailed( + reason = validitySpec.getWhyNotSatisfied(node) + ) + } + + createdArgumentsCount.incrementAndGet() + + return node + } + + /** + * ์ •์ˆ˜ ๊ฐ’์œผ๋กœ ์ˆซ์ž ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ •์ˆ˜ ๊ฐ’ + * @return NumberNode ์ธ์Šคํ„ด์Šค + */ + fun createNumber(value: Int): NumberNode = createNumber(value.toDouble()) + + /** + * Long ๊ฐ’์œผ๋กœ ์ˆซ์ž ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value Long ๊ฐ’ + * @return NumberNode ์ธ์Šคํ„ด์Šค + */ + fun createNumber(value: Long): NumberNode = createNumber(value.toDouble()) + + /** + * Float ๊ฐ’์œผ๋กœ ์ˆซ์ž ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value Float ๊ฐ’ + * @return NumberNode ์ธ์Šคํ„ด์Šค + */ + fun createNumber(value: Float): NumberNode = createNumber(value.toDouble()) + + /** + * ๋ฌธ์ž์—ด์—์„œ ์ˆซ์ž ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ˆซ์ž ๋ฌธ์ž์—ด + * @return NumberNode ์ธ์Šคํ„ด์Šค + * @throws NumberFormatException ์œ ํšจํ•˜์ง€ ์•Š์€ ์ˆซ์ž ํ˜•์‹์ธ ๊ฒฝ์šฐ + */ + fun createNumberFromString(value: String): NumberNode { + val doubleValue = value.toDoubleOrNull() + ?: throw ASTException.invalidNumberLiteral(value) + return createNumber(doubleValue) + } + + /** + * ๋ฌธ์ž์—ด์—์„œ ๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ถˆ๋ฆฌ์–ธ ๋ฌธ์ž์—ด + * @return BooleanNode ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ถˆ๋ฆฌ์–ธ ํ˜•์‹์ธ ๊ฒฝ์šฐ + */ + fun createBooleanFromString(value: String): BooleanNode { + val booleanValue = when (value.lowercase()) { + "true", "1", "yes", "y", "on" -> true + "false", "0", "no", "n", "off" -> false + else -> throw ASTException.invalidBooleanValue(value) + } + return createBoolean(booleanValue) + } + + /** + * ์‚ฐ์ˆ  ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @param operator ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž + * @param right ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @return BinaryOpNode ์ธ์Šคํ„ด์Šค + */ + fun createArithmeticOp(left: ASTNode, operator: String, right: ASTNode): BinaryOpNode { + if (operator !in ARITHMETIC_OPERATORS) { + throw ASTException.notArithmeticOperator(operator) + } + return createBinaryOp(left, operator, right) + } + + /** + * ๋น„๊ต ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @param operator ๋น„๊ต ์—ฐ์‚ฐ์ž + * @param right ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @return BinaryOpNode ์ธ์Šคํ„ด์Šค + */ + fun createComparisonOp(left: ASTNode, operator: String, right: ASTNode): BinaryOpNode { + if (operator !in COMPARISON_OPERATORS) { + throw ASTException.notComparisonOperator(operator) + } + return createBinaryOp(left, operator, right) + } + + /** + * ๋…ผ๋ฆฌ ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @param operator ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž + * @param right ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @return BinaryOpNode ์ธ์Šคํ„ด์Šค + */ + fun createLogicalOp(left: ASTNode, operator: String, right: ASTNode): BinaryOpNode { + if (operator !in LOGICAL_OPERATORS) { + throw ASTException.notLogicalOperator(operator) + } + return createBinaryOp(left, operator, right) + } + + /** + * ๋‹จํ•ญ ๋งˆ์ด๋„ˆ์Šค ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operand ํ”ผ์—ฐ์‚ฐ์ž + * @return UnaryOpNode ์ธ์Šคํ„ด์Šค + */ + fun createUnaryMinus(operand: ASTNode): UnaryOpNode = createUnaryOp(MINUS, operand) + + /** + * ๋‹จํ•ญ ํ”Œ๋Ÿฌ์Šค ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operand ํ”ผ์—ฐ์‚ฐ์ž + * @return UnaryOpNode ์ธ์Šคํ„ด์Šค + */ + fun createUnaryPlus(operand: ASTNode): UnaryOpNode = createUnaryOp(PLUS, operand) + + /** + * ๋…ผ๋ฆฌ ๋ถ€์ • ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operand ํ”ผ์—ฐ์‚ฐ์ž + * @return UnaryOpNode ์ธ์Šคํ„ด์Šค + */ + fun createLogicalNot(operand: ASTNode): UnaryOpNode = createUnaryOp(EXCLAMATION, operand) + + /** + * ์ˆ˜ํ•™ ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์ˆ˜ํ•™ ํ•จ์ˆ˜๋ช… + * @param args ์ธ์ˆ˜ ๋ชฉ๋ก + * @return FunctionCallNode ์ธ์Šคํ„ด์Šค + */ + fun createMathFunction(name: String, args: List): FunctionCallNode { + if (!SUPPORTED_MATH_FUNCTIONS.contains(name.uppercase())) { + throw ASTException.unsupportedMathFunction(name) + } + return createFunctionCall(name.uppercase(), args) + } + + /** + * ์‚ผํ•ญ ์—ฐ์‚ฐ์ž ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param condition ์กฐ๊ฑด์‹ + * @param trueValue ์ฐธ ๊ฐ’ + * @param falseValue ๊ฑฐ์ง“ ๊ฐ’ + * @return IfNode ์ธ์Šคํ„ด์Šค + */ + fun createTernary(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode): IfNode { + return createIf(condition, trueValue, falseValue) + } + + /** + * ์ตœ์ ํ™”๋œ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ์ตœ์ ํ™”ํ•  ๋…ธ๋“œ + * @return ์ตœ์ ํ™”๋œ ๋…ธ๋“œ + */ + fun createOptimized(node: ASTNode): ASTNode { + return when (node) { + is IfNode -> node.optimize() + is UnaryOpNode -> node.simplify() + else -> node + } + } + + /** + * ํŒฉํ† ๋ฆฌ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒฉํ† ๋ฆฌ ์‚ฌ์šฉ ํ†ต๊ณ„ + */ + fun getFactoryStatistics(): Map { + return mapOf( + "totalNodesCreated" to createdNodeCount.get(), + "numberNodesCreated" to createdNumberCount.get(), + "booleanNodesCreated" to createdBooleanCount.get(), + "variableNodesCreated" to createdVariableCount.get(), + "binaryOpNodesCreated" to createdBinaryOpCount.get(), + "unaryOpNodesCreated" to createdUnaryOpCount.get(), + "functionCallNodesCreated" to createdFunctionCallCount.get(), + "ifNodesCreated" to createdIfCount.get(), + "argumentsNodesCreated" to createdArgumentsCount.get(), + "factoryComplexity" to Complexity.HIGH.name, + "cacheEnabled" to true + ) + } + + + init { + createdNodeCount.incrementAndGet() + } + + private fun validateNodeAfterBuild(node: ASTNode) { + // ์ƒ์„ฑ ํ›„ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + if (!validitySpec.isSatisfiedBy(node)) { + throw ASTException.nodeValidationFailed( + reason = validitySpec.getWhyNotSatisfied(node) + ) + } + + // ๊ตฌ์กฐ ๊ฒ€์ฆ + if (!structureSpec.isSatisfiedBy(node)) { + throw ASTException.nodeStructureFailed( + reason = structureSpec.getWhyNotSatisfied(node) + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilderContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilderContract.kt new file mode 100644 index 00000000..45bb3a5f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilderContract.kt @@ -0,0 +1,79 @@ +package hs.kr.entrydsm.domain.ast.factory + +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity + +/** + * AST ๋…ธ๋“œ ๋นŒ๋”์˜ ๊ธฐ๋ณธ ๊ณ„์•ฝ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ํŒŒ์„œ์—์„œ ์ƒ์„ฑ ๊ทœ์น™(Production)์„ ์ ์šฉํ•  ๋•Œ ํ•ด๋‹นํ•˜๋Š” AST ๋…ธ๋“œ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” + * ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ํŒจํ„ด์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ์ƒ์„ฑ ๊ทœ์น™๋งˆ๋‹ค ๋Œ€์‘ํ•˜๋Š” ๋นŒ๋”๊ฐ€ ์žˆ์œผ๋ฉฐ, + * ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ์ƒ์„ฑ๋œ ์ž์‹ ์‹ฌ๋ณผ๋“ค์„ ์กฐํ•ฉํ•˜์—ฌ ์ ์ ˆํ•œ AST ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Factory(context = "ast", complexity = Complexity.NORMAL, cache = false) +interface ASTBuilderContract { + + /** + * ์ž์‹ ์‹ฌ๋ณผ๋“ค๋กœ๋ถ€ํ„ฐ AST ๋…ธ๋“œ ๋˜๋Š” ์‹ฌ๋ณผ ๋ฆฌ์ŠคํŠธ๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + * + * ํŒŒ์„œ์˜ reduce ๋™์ž‘์—์„œ ํ˜ธ์ถœ๋˜๋ฉฐ, ์ƒ์„ฑ ๊ทœ์น™์˜ ์šฐ๋ณ€์— ํ•ด๋‹นํ•˜๋Š” + * ์ž์‹ ์‹ฌ๋ณผ๋“ค์„ ๋ฐ›์•„์„œ ์ขŒ๋ณ€์— ํ•ด๋‹นํ•˜๋Š” AST ๋…ธ๋“œ๋‚˜ ์‹ฌ๋ณผ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๋ฐ˜ํ™˜ ํƒ€์ž…์€ ์ƒ์„ฑ ๊ทœ์น™์— ๋”ฐ๋ผ ASTNode ๋˜๋Š” List์ž…๋‹ˆ๋‹ค. + * + * @param children ์ƒ์„ฑ ๊ทœ์น™์˜ ์šฐ๋ณ€์— ํ•ด๋‹นํ•˜๋Š” ์ž์‹ ์‹ฌ๋ณผ ๋ชฉ๋ก + * @return ๊ตฌ์ถ•๋œ AST ๋…ธ๋“œ ๋˜๋Š” ์‹ฌ๋ณผ ๋ฆฌ์ŠคํŠธ + * @throws IllegalArgumentException ์ž์‹ ์‹ฌ๋ณผ์˜ ๊ฐœ์ˆ˜๋‚˜ ํƒ€์ž…์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ๊ฒฝ์šฐ + * @throws ClassCastException ์ž์‹ ์‹ฌ๋ณผ์˜ ํƒ€์ž… ๋ณ€ํ™˜์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ + */ + fun build(children: List): Any + + /** + * ๋นŒ๋”๊ฐ€ ์š”๊ตฌํ•˜๋Š” ์ตœ์†Œ ์ž์‹ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ตœ์†Œ ์ž์‹ ๊ฐœ์ˆ˜ + */ + fun getMinimumChildrenCount(): Int = 0 + + /** + * ๋นŒ๋”๊ฐ€ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š” ์ตœ๋Œ€ ์ž์‹ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ตœ๋Œ€ ์ž์‹ ๊ฐœ์ˆ˜ (๋ฌด์ œํ•œ์ธ ๊ฒฝ์šฐ -1) + */ + fun getMaximumChildrenCount(): Int = -1 + + /** + * ์ž์‹ ์‹ฌ๋ณผ๋“ค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param children ๊ฒ€์‚ฌํ•  ์ž์‹ ์‹ฌ๋ณผ๋“ค + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun validateChildren(children: List): Boolean { + val count = children.size + val minCount = getMinimumChildrenCount() + val maxCount = getMaximumChildrenCount() + + if (count < minCount) return false + if (maxCount >= 0 && count > maxCount) return false + + return true + } + + /** + * ๋นŒ๋”์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋นŒ๋” ์ด๋ฆ„ + */ + fun getBuilderName(): String = this::class.simpleName ?: "UnknownBuilder" + + /** + * ๋นŒ๋”์˜ ์„ค๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋นŒ๋” ์„ค๋ช… + */ + fun getDescription(): String = "AST ๋…ธ๋“œ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋นŒ๋”์ž…๋‹ˆ๋‹ค" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilders.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilders.kt new file mode 100644 index 00000000..d2588d56 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/ASTBuilders.kt @@ -0,0 +1,197 @@ +package hs.kr.entrydsm.domain.ast.factory + +import hs.kr.entrydsm.domain.ast.factory.builders.* +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity + +/** + * AST ๋นŒ๋”๋“ค์„ ๊ด€๋ฆฌํ•˜๋Š” ํŒฉํ† ๋ฆฌ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ๋ชจ๋“  ๋นŒ๋”๋Š” ๊ฐœ๋ณ„ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ๋˜์–ด ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์„ ์ค€์ˆ˜ํ•˜๋ฉฐ, + * ์ด ํด๋ž˜์Šค๋Š” ๋นŒ๋”๋“ค์˜ ์ƒ์„ฑ๊ณผ ์ ‘๊ทผ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.HIGH, cache = true) +object ASTBuilders { + + /** + * ํ•ญ๋“ฑ ๋นŒ๋” - ์ฒซ ๋ฒˆ์งธ ์ž์‹์„ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + val Identity = IdentityBuilder + + /** + * ์‹œ์ž‘ ๋นŒ๋” - ๋ฌธ๋ฒ•์˜ ์‹œ์ž‘ ์‹ฌ๋ณผ์šฉ ๋นŒ๋”์ž…๋‹ˆ๋‹ค. + */ + val Start = StartBuilder + + /** + * ์ˆซ์ž ๋นŒ๋” - ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + val Number = NumberBuilder + + /** + * ๋ณ€์ˆ˜ ๋นŒ๋” - ๋ณ€์ˆ˜ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + val Variable = VariableBuilder + + /** + * TRUE ๋ถˆ๋ฆฐ ๋นŒ๋” - true ๋ถˆ๋ฆฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + val BooleanTrue = BooleanTrueBuilder + + /** + * FALSE ๋ถˆ๋ฆฐ ๋นŒ๋” - false ๋ถˆ๋ฆฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + val BooleanFalse = BooleanFalseBuilder + + /** + * ๊ด„ํ˜ธ ๋นŒ๋” - ๊ด„ํ˜ธ๋กœ ๋‘˜๋Ÿฌ์‹ธ์ธ ํ‘œํ˜„์‹์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + val Parenthesized = ParenthesizedBuilder + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋นŒ๋” - ์ธ์ˆ˜๊ฐ€ ์žˆ๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + val FunctionCall = FunctionCallBuilder + + /** + * ๋นˆ ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋นŒ๋” - ์ธ์ˆ˜๊ฐ€ ์—†๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + val FunctionCallEmpty = FunctionCallEmptyBuilder + + /** + * IF ์กฐ๊ฑด๋ฌธ ๋นŒ๋” - IF ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + val If = IfBuilder + + /** + * ๋‹จ์ผ ์ธ์ˆ˜ ๋นŒ๋” - ๋‹จ์ผ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + val ArgsSingle = ArgsSingleBuilder + + /** + * ๋‹ค์ค‘ ์ธ์ˆ˜ ๋นŒ๋” - ๊ธฐ์กด ์ธ์ˆ˜ ๋ชฉ๋ก์— ์ƒˆ ์ธ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + */ + val ArgsMultiple = ArgsMultipleBuilder + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ์ž ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @param leftIndex ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž ์ธ๋ฑ์Šค + * @param rightIndex ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž ์ธ๋ฑ์Šค + * @return BinaryOp ๋นŒ๋” ์ธ์Šคํ„ด์Šค + */ + fun createBinaryOp(operator: String, leftIndex: Int = 0, rightIndex: Int = 2): BinaryOpBuilder = + BinaryOpBuilder(operator, leftIndex, rightIndex) + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @param operandIndex ํ”ผ์—ฐ์‚ฐ์ž ์ธ๋ฑ์Šค + * @return UnaryOp ๋นŒ๋” ์ธ์Šคํ„ด์Šค + */ + fun createUnaryOp(operator: String, operandIndex: Int = 1): UnaryOpBuilder = + UnaryOpBuilder(operator, operandIndex) + + /** + * POC ์ฝ”๋“œ์˜ 34๊ฐœ ์ƒ์‚ฐ ๊ทœ์น™์— ๋Œ€์‘ํ•˜๋Š” ๋ชจ๋“  AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์‚ฐ ๊ทœ์น™ ID -> ๋นŒ๋” ์ธ์Šคํ„ด์Šค ๋งคํ•‘ + */ + fun getProductionBuilders(): Map = mapOf( + // 0: EXPR โ†’ EXPR || AND_EXPR + 0 to createBinaryOp("||", 0, 2), + // 1: EXPR โ†’ AND_EXPR + 1 to Identity, + // 2: AND_EXPR โ†’ AND_EXPR && COMP_EXPR + 2 to createBinaryOp("&&", 0, 2), + // 3: AND_EXPR โ†’ COMP_EXPR + 3 to Identity, + // 4: COMP_EXPR โ†’ COMP_EXPR == ARITH_EXPR + 4 to createBinaryOp("==", 0, 2), + // 5: COMP_EXPR โ†’ COMP_EXPR != ARITH_EXPR + 5 to createBinaryOp("!=", 0, 2), + // 6: COMP_EXPR โ†’ COMP_EXPR < ARITH_EXPR + 6 to createBinaryOp("<", 0, 2), + // 7: COMP_EXPR โ†’ COMP_EXPR <= ARITH_EXPR + 7 to createBinaryOp("<=", 0, 2), + // 8: COMP_EXPR โ†’ COMP_EXPR > ARITH_EXPR + 8 to createBinaryOp(">", 0, 2), + // 9: COMP_EXPR โ†’ COMP_EXPR >= ARITH_EXPR + 9 to createBinaryOp(">=", 0, 2), + // 10: COMP_EXPR โ†’ ARITH_EXPR + 10 to Identity, + // 11: ARITH_EXPR โ†’ ARITH_EXPR + TERM + 11 to createBinaryOp("+", 0, 2), + // 12: ARITH_EXPR โ†’ ARITH_EXPR - TERM + 12 to createBinaryOp("-", 0, 2), + // 13: ARITH_EXPR โ†’ TERM + 13 to Identity, + // 14: TERM โ†’ TERM * FACTOR + 14 to createBinaryOp("*", 0, 2), + // 15: TERM โ†’ TERM / FACTOR + 15 to createBinaryOp("/", 0, 2), + // 16: TERM โ†’ TERM % FACTOR + 16 to createBinaryOp("%", 0, 2), + // 17: TERM โ†’ FACTOR + 17 to Identity, + // 18: FACTOR โ†’ PRIMARY ^ FACTOR (์šฐ๊ฒฐํ•ฉ) + 18 to createBinaryOp("^", 0, 2), + // 19: FACTOR โ†’ PRIMARY + 19 to Identity, + // 20: PRIMARY โ†’ ( EXPR ) + 20 to Parenthesized, + // 21: PRIMARY โ†’ - PRIMARY + 21 to createUnaryOp("-", 1), + // 22: PRIMARY โ†’ + PRIMARY + 22 to createUnaryOp("+", 1), + // 23: PRIMARY โ†’ ! PRIMARY + 23 to createUnaryOp("!", 1), + // 24: PRIMARY โ†’ NUMBER + 24 to Number, + // 25: PRIMARY โ†’ VARIABLE + 25 to Variable, + // 26: PRIMARY โ†’ IDENTIFIER + 26 to Variable, + // 27: PRIMARY โ†’ TRUE + 27 to BooleanTrue, + // 28: PRIMARY โ†’ FALSE + 28 to BooleanFalse, + // 29: PRIMARY โ†’ IDENTIFIER ( ARGS ) + 29 to FunctionCall, + // 30: PRIMARY โ†’ IDENTIFIER ( ) + 30 to FunctionCallEmpty, + // 31: PRIMARY โ†’ IF ( EXPR , EXPR , EXPR ) + 31 to If, + // 32: ARGS โ†’ EXPR + 32 to ArgsSingle, + // 33: ARGS โ†’ ARGS , EXPR + 33 to ArgsMultiple + ) + + /** + * ๋ชจ๋“  ๋นŒ๋”์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋นŒ๋” ์ธ์Šคํ„ด์Šค๋“ค์˜ ๋งต (์ด๋ฆ„ -> ์ธ์Šคํ„ด์Šค) + */ + fun getAllBuilders(): Map = mapOf( + "Identity" to Identity, + "Start" to Start, + "Number" to Number, + "Variable" to Variable, + "BooleanTrue" to BooleanTrue, + "BooleanFalse" to BooleanFalse, + "Parenthesized" to Parenthesized, + "FunctionCall" to FunctionCall, + "FunctionCallEmpty" to FunctionCallEmpty, + "If" to If, + "ArgsSingle" to ArgsSingle, + "ArgsMultiple" to ArgsMultiple + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsMultipleBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsMultipleBuilder.kt new file mode 100644 index 00000000..bc00f3cc --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsMultipleBuilder.kt @@ -0,0 +1,47 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ๋‹ค์ค‘ ์ธ์ˆ˜ ๋นŒ๋” - ๊ธฐ์กด ์ธ์ˆ˜ ๋ชฉ๋ก์— ์ƒˆ ์ธ์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * ๊ธฐ์กด ์ธ์ˆ˜ ๋ชฉ๋ก๊ณผ ์ƒˆ๋กœ์šด ์ธ์ˆ˜๋ฅผ ๊ฒฐํ•ฉํ•˜์—ฌ ํ™•์žฅ๋œ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์˜ˆ: ARGS , EXPR -> Args + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Policy( + name = "Multiple Arguments Policy", + description = "๋‹ค์ค‘ ์ธ์ˆ˜๋Š” ๊ธฐ์กด ์ธ์ˆ˜ ๋ชฉ๋ก๊ณผ ์ƒˆ๋กœ์šด ์ธ์ˆ˜๋ฅผ ์‰ผํ‘œ๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ๊ฒฐํ•ฉํ•ด์•ผ ํ•จ", + domain = "ast", + scope = Scope.AGGREGATE +) +object ArgsMultipleBuilder : ASTBuilderContract { + override fun build(children: List): List { + if (children.size != 3) { + throw ASTException.argsMultipleChildrenMismatch(actual = children.size) + } + @Suppress("UNCHECKED_CAST") + val existingArgs = children[0] as List + val newArg = children[2] as ASTNode + + return existingArgs + newArg + } + + override fun validateChildren(children: List): Boolean { + return children.size == 3 && + children[0] is List<*> && + (children[0] as List<*>).all { it is ASTNode } && + children[2] is ASTNode + } + + override fun getBuilderName(): String = "ArgsMultiple" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsSingleBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsSingleBuilder.kt new file mode 100644 index 00000000..6b8c4af5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ArgsSingleBuilder.kt @@ -0,0 +1,40 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * ๋‹จ์ผ ์ธ์ˆ˜ ๋นŒ๋” - ๋‹จ์ผ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * ํ•˜๋‚˜์˜ ํ‘œํ˜„์‹์„ ์ธ์ˆ˜ ๋ชฉ๋ก์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * ์˜ˆ: EXPR -> Args + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Specification( + name = "Single Argument Specification", + description = "๋‹จ์ผ ์ธ์ˆ˜๋Š” ํ•˜๋‚˜์˜ ์œ ํšจํ•œ ํ‘œํ˜„์‹์ด์–ด์•ผ ํ•จ", + domain = "ast", + priority = Priority.NORMAL +) +object ArgsSingleBuilder : ASTBuilderContract { + override fun build(children: List): List { + if (children.size != 1) { + throw ASTException.argsSingleChildMismatch(actual = children.size) + } + return listOf(children[0] as ASTNode) + } + + override fun validateChildren(children: List): Boolean { + return children.size == 1 && children[0] is ASTNode + } + + override fun getBuilderName(): String = "ArgsSingle" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BinaryOpBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BinaryOpBuilder.kt new file mode 100644 index 00000000..773f96d8 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BinaryOpBuilder.kt @@ -0,0 +1,72 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ์ดํ•ญ ์—ฐ์‚ฐ์ž ๋นŒ๋” - ์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @property operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @property leftIndex ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž์˜ ์ธ๋ฑ์Šค (๊ธฐ๋ณธ๊ฐ’: 0) + * @property rightIndex ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž์˜ ์ธ๋ฑ์Šค (๊ธฐ๋ณธ๊ฐ’: 2) + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.NORMAL, cache = false) +@Policy( + name = "Binary Operator Policy", + description = "์ดํ•ญ ์—ฐ์‚ฐ์ž๋Š” ์ •ํ™•ํžˆ ๋‘ ๊ฐœ์˜ ํ”ผ์—ฐ์‚ฐ์ž๋ฅผ ๊ฐ€์ ธ์•ผ ํ•˜๋ฉฐ, ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ค€์ˆ˜ํ•ด์•ผ ํ•จ", + domain = "ast", + scope = Scope.AGGREGATE +) +class BinaryOpBuilder( + private val operator: String, + private val leftIndex: Int = 0, + private val rightIndex: Int = 2 +) : ASTBuilderContract { + + override fun build(children: List): BinaryOpNode { + val required = maxOf(leftIndex, rightIndex) + 1 + if (children.size < required) { + throw ASTException.binaryChildrenInsufficient( + required = required, + actual = children.size, + leftIndex = leftIndex, + rightIndex = rightIndex + ) + } + + val left = children[leftIndex] + if (left !is ASTNode) { + throw ASTException.operandNotAst( + side = "left", + actualType = left::class.simpleName + ) + } + + val right = children[rightIndex] + if (right !is ASTNode) { + throw ASTException.operandNotAst( + side = "right", + actualType = right::class.simpleName + ) + } + + return BinaryOpNode(left, operator, right) + } + + override fun validateChildren(children: List): Boolean { + return children.size >= maxOf(leftIndex, rightIndex) + 1 && + leftIndex < children.size && children[leftIndex] is ASTNode && + rightIndex < children.size && children[rightIndex] is ASTNode + } + + override fun getBuilderName(): String = "BinaryOp($operator)" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanFalseBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanFalseBuilder.kt new file mode 100644 index 00000000..3d6d1ca9 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanFalseBuilder.kt @@ -0,0 +1,33 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.BooleanNode +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * FALSE ๋ถˆ๋ฆฐ ๋นŒ๋” - false ๋ถˆ๋ฆฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Specification( + name = "Boolean False Specification", + description = "false ๋ถˆ๋ฆฐ ๋ฆฌํ„ฐ๋Ÿด์€ ํ•ญ์ƒ ๊ฑฐ์ง“ ๊ฐ’์„ ๋‚˜ํƒ€๋‚ด์•ผ ํ•จ", + domain = "ast", + priority = Priority.NORMAL +) +object BooleanFalseBuilder : ASTBuilderContract { + override fun build(children: List): BooleanNode { + return BooleanNode.FALSE + } + + override fun validateChildren(children: List): Boolean { + return true // ์ž์‹ ์š”์†Œ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Œ + } + + override fun getBuilderName(): String = "BooleanFalse" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanTrueBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanTrueBuilder.kt new file mode 100644 index 00000000..df4b0212 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/BooleanTrueBuilder.kt @@ -0,0 +1,33 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.BooleanNode +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * TRUE ๋ถˆ๋ฆฐ ๋นŒ๋” - true ๋ถˆ๋ฆฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Specification( + name = "Boolean True Specification", + description = "true ๋ถˆ๋ฆฐ ๋ฆฌํ„ฐ๋Ÿด์€ ํ•ญ์ƒ ์ฐธ ๊ฐ’์„ ๋‚˜ํƒ€๋‚ด์•ผ ํ•จ", + domain = "ast", + priority = Priority.NORMAL +) +object BooleanTrueBuilder : ASTBuilderContract { + override fun build(children: List): BooleanNode { + return BooleanNode.TRUE + } + + override fun validateChildren(children: List): Boolean { + return true // ์ž์‹ ์š”์†Œ๊ฐ€ ํ•„์š”ํ•˜์ง€ ์•Š์Œ + } + + override fun getBuilderName(): String = "BooleanTrue" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallBuilder.kt new file mode 100644 index 00000000..fa748840 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallBuilder.kt @@ -0,0 +1,59 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋นŒ๋” - ์ธ์ˆ˜๊ฐ€ ์žˆ๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * ํ•จ์ˆ˜๋ช…๊ณผ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ๋ฐ›์•„์„œ FunctionCallNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์˜ˆ: IDENTIFIER ( ARGS ) -> FunctionCall + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.NORMAL, cache = false) +@Policy( + name = "Function Call Policy", + description = "ํ•จ์ˆ˜ ํ˜ธ์ถœ์€ ์œ ํšจํ•œ ํ•จ์ˆ˜๋ช…๊ณผ ์ ์ ˆํ•œ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์•ผ ํ•จ", + domain = "ast", + scope = Scope.AGGREGATE +) +object FunctionCallBuilder : ASTBuilderContract { + override fun build(children: List): FunctionCallNode { + if (children.size != 3) { + throw ASTException.functionCallChildrenMismatch(actual = children.size) + } + if (children[0] !is Token) { + throw ASTException.functionCallFirstNotToken(children[0]::class.simpleName) + } + if (children[2] !is List<*>) { + throw ASTException.functionCallThirdNotList(children[2]::class.simpleName) + } + if (!(children[2] as List<*>).all { it is ASTNode }) { + throw ASTException.functionCallArgsNotAstNode() + } + + + val nameToken = children[0] as Token + val args = children[2] as List + + return FunctionCallNode(nameToken.value, args) + } + + override fun validateChildren(children: List): Boolean { + return children.size == 3 && + children[0] is Token && + children[2] is List<*> && + (children[2] as List<*>).all { it is ASTNode } + } + + override fun getBuilderName(): String = "FunctionCall" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallEmptyBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallEmptyBuilder.kt new file mode 100644 index 00000000..91ad437d --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/FunctionCallEmptyBuilder.kt @@ -0,0 +1,56 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * ๋นˆ ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋นŒ๋” - ์ธ์ˆ˜๊ฐ€ ์—†๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * ํ•จ์ˆ˜๋ช…๋งŒ ๋ฐ›์•„์„œ ๋นˆ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ๊ฐ€์ง„ FunctionCallNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์˜ˆ: IDENTIFIER ( ) -> FunctionCall + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Specification( + name = "Empty Function Call Specification", + description = "์ธ์ˆ˜๊ฐ€ ์—†๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ์€ ํ•จ์ˆ˜๋ช…๊ณผ ๋นˆ ๊ด„ํ˜ธ๋กœ ๊ตฌ์„ฑ๋˜์–ด์•ผ ํ•จ", + domain = "ast", + priority = Priority.NORMAL +) +object FunctionCallEmptyBuilder : ASTBuilderContract { + override fun build(children: List): FunctionCallNode { + if (children.size != 3) { + throw ASTException.functionCallEmptyChildrenMismatch(actual = children.size) + } + if (children[0] !is Token) { + throw ASTException.functionCallEmptyFirstNotToken(children[0]::class.simpleName) + } + if (children[1] !is Token) { + throw ASTException.functionCallEmptySecondNotToken(children[1]::class.simpleName) + } + if (children[2] !is Token) { + throw ASTException.functionCallEmptyThirdNotToken(children[2]::class.simpleName) + } + + + val nameToken = children[0] as Token + return FunctionCallNode(nameToken.value, emptyList()) + } + + override fun validateChildren(children: List): Boolean { + return children.size == 3 && + children[0] is Token && // IDENTIFIER + children[1] is Token && // LEFT_PAREN + children[2] is Token // RIGHT_PAREN + } + + override fun getBuilderName(): String = "FunctionCallEmpty" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IdentityBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IdentityBuilder.kt new file mode 100644 index 00000000..9e901569 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IdentityBuilder.kt @@ -0,0 +1,44 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * ํ•ญ๋“ฑ ๋นŒ๋” - ์ฒซ ๋ฒˆ์งธ ์ž์‹์„ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * ์ฃผ๋กœ ๋‹จ์ผ ํ•ญ๋ชฉ์„ ๊ฐ์‹ธ๋Š” ์ƒ์„ฑ ๊ทœ์น™์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * ์˜ˆ: EXPR -> AND_EXPR + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Specification( + name = "Identity Build Rule", + description = "๋‹จ์ผ ์ž์‹ ๋…ธ๋“œ๋ฅผ ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•ญ๋“ฑ ๋ณ€ํ™˜ ๊ทœ์น™", + domain = "ast", + priority = Priority.NORMAL +) +object IdentityBuilder : ASTBuilderContract { + override fun build(children: List): ASTNode { + if (children.isEmpty()) { + throw ASTException.identityChildrenEmpty(actual = children.size) + } + if (children[0] !is ASTNode) { + throw ASTException.identityFirstNotAstNode(children[0]::class.simpleName) + } + + return children[0] as ASTNode + } + + override fun validateChildren(children: List): Boolean { + return children.size == 1 && children[0] is ASTNode + } + + override fun getBuilderName(): String = "Identity" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IfBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IfBuilder.kt new file mode 100644 index 00000000..0f6219fb --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/IfBuilder.kt @@ -0,0 +1,48 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.IfNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * IF ์กฐ๊ฑด๋ฌธ ๋นŒ๋” - IF ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * IF(์กฐ๊ฑด, ์ฐธ๊ฐ’, ๊ฑฐ์ง“๊ฐ’) ํ˜•ํƒœ์˜ ์กฐ๊ฑด๋ฌธ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * ์˜ˆ: IF ( EXPR , EXPR , EXPR ) -> If + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.HIGH, cache = false) +@Policy( + name = "Conditional Expression Policy", + description = "IF ์กฐ๊ฑด๋ฌธ์€ ์กฐ๊ฑด์‹, ์ฐธ๊ฐ’, ๊ฑฐ์ง“๊ฐ’์„ ๋ชจ๋‘ ๊ฐ€์ ธ์•ผ ํ•˜๋ฉฐ ์ ์ ˆํ•œ ํ˜•์‹์ด์–ด์•ผ ํ•จ", + domain = "ast", + scope = Scope.AGGREGATE +) +object IfBuilder : ASTBuilderContract { + override fun build(children: List): IfNode { + if (children.size != 8) { + throw ASTException.ifChildrenMismatch(8, children.size) + } + val condition = children[2] as ASTNode + val trueValue = children[4] as ASTNode + val falseValue = children[6] as ASTNode + + return IfNode(condition, trueValue, falseValue) + } + + override fun validateChildren(children: List): Boolean { + return children.size == 8 && + children[2] is ASTNode && + children[4] is ASTNode && + children[6] is ASTNode + } + + override fun getBuilderName(): String = "If" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/NumberBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/NumberBuilder.kt new file mode 100644 index 00000000..dee8df25 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/NumberBuilder.kt @@ -0,0 +1,45 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.NumberNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * ์ˆซ์ž ๋นŒ๋” - ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * ํ† ํฐ์˜ ๊ฐ’์„ Double๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ NumberNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Specification( + name = "Number Literal Specification", + description = "์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด์€ ์œ ํšจํ•œ ์ˆซ์ž ํ˜•์‹์ด์–ด์•ผ ํ•˜๋ฉฐ Double๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•ด์•ผ ํ•จ", + domain = "ast", + priority = Priority.HIGH +) +object NumberBuilder : ASTBuilderContract { + override fun build(children: List): NumberNode { + if (children.size != 1) { + throw ASTException.numberChildrenMismatch(1, children.size) + } + val token = children[0] as Token + val value = token.value.toDoubleOrNull() + ?: throw ASTException.invalidNumberLiteral(token.value) + + return NumberNode(value) + } + + override fun validateChildren(children: List): Boolean { + return children.size == 1 && children[0] is Token && + (children[0] as Token).value.toDoubleOrNull() != null + } + + override fun getBuilderName(): String = "Number" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ParenthesizedBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ParenthesizedBuilder.kt new file mode 100644 index 00000000..5f6b410b --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/ParenthesizedBuilder.kt @@ -0,0 +1,44 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ๊ด„ํ˜ธ ๋นŒ๋” - ๊ด„ํ˜ธ๋กœ ๋‘˜๋Ÿฌ์‹ธ์ธ ํ‘œํ˜„์‹์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * ๊ด„ํ˜ธ ์•ˆ์˜ ํ‘œํ˜„์‹์„ ์ถ”์ถœํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * ์˜ˆ: ( EXPR ) -> EXPR + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Policy( + name = "Parenthesized Expression Policy", + description = "๊ด„ํ˜ธ๋กœ ๋‘˜๋Ÿฌ์‹ธ์ธ ํ‘œํ˜„์‹์€ ์ขŒ๊ด„ํ˜ธ, ํ‘œํ˜„์‹, ์šฐ๊ด„ํ˜ธ ์ˆœ์„œ๋กœ ๊ตฌ์„ฑ๋˜์–ด์•ผ ํ•จ", + domain = "ast", + scope = Scope.AGGREGATE +) +object ParenthesizedBuilder : ASTBuilderContract { + override fun build(children: List): ASTNode { + if (children.size != 3) { + throw ASTException.parenthesizedChildrenMismatch(3, children.size) + } + if (children[1] !is ASTNode) { + throw ASTException.parenthesizedSecondNotAst(children[1]::class.simpleName) + } + + return children[1] as ASTNode + } + + override fun validateChildren(children: List): Boolean { + return children.size == 3 && children[1] is ASTNode + } + + override fun getBuilderName(): String = "Parenthesized" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/StartBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/StartBuilder.kt new file mode 100644 index 00000000..f5caba92 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/StartBuilder.kt @@ -0,0 +1,44 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ์‹œ์ž‘ ๋นŒ๋” - ๋ฌธ๋ฒ•์˜ ์‹œ์ž‘ ์‹ฌ๋ณผ์šฉ ๋นŒ๋”์ž…๋‹ˆ๋‹ค. + * + * ํ™•์žฅ๋œ ๋ฌธ๋ฒ•์˜ ์‹œ์ž‘ ๊ทœ์น™์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * ์˜ˆ: START -> EXPR + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Policy( + name = "Start Symbol Policy", + description = "๋ฌธ๋ฒ•์˜ ์‹œ์ž‘ ์‹ฌ๋ณผ์€ ์ •ํ™•ํžˆ ํ•˜๋‚˜์˜ ์ž์‹ ๋…ธ๋“œ๋ฅผ ๊ฐ€์ ธ์•ผ ํ•จ", + domain = "ast", + scope = Scope.DOMAIN +) +object StartBuilder : ASTBuilderContract { + override fun build(children: List): ASTNode { + if (children.size != 1) { + throw ASTException.startChildrenMismatch(1, children.size) + } + if (children[0] !is ASTNode) { + throw ASTException.startFirstNotAst(children[0]::class.simpleName) + } + + return children[0] as ASTNode + } + + override fun validateChildren(children: List): Boolean { + return children.size == 1 && children[0] is ASTNode + } + + override fun getBuilderName(): String = "Start" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/UnaryOpBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/UnaryOpBuilder.kt new file mode 100644 index 00000000..4083a7c1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/UnaryOpBuilder.kt @@ -0,0 +1,49 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋นŒ๋” - ๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @property operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @property operandIndex ํ”ผ์—ฐ์‚ฐ์ž์˜ ์ธ๋ฑ์Šค (๊ธฐ๋ณธ๊ฐ’: 1) + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.NORMAL, cache = false) +@Specification( + name = "Unary Operator Specification", + description = "๋‹จํ•ญ ์—ฐ์‚ฐ์ž๋Š” ์ •ํ™•ํžˆ ํ•˜๋‚˜์˜ ํ”ผ์—ฐ์‚ฐ์ž๋ฅผ ๊ฐ€์ ธ์•ผ ํ•จ", + domain = "ast", + priority = Priority.HIGH +) +class UnaryOpBuilder( + private val operator: String, + private val operandIndex: Int = 1 +) : ASTBuilderContract { + + override fun build(children: List): UnaryOpNode { + val required = operandIndex + 1 + if (children.size < required) { + throw ASTException.unaryChildrenInsufficient(required, children.size, operandIndex) + } + + val operand = children[operandIndex] as ASTNode + return UnaryOpNode(operator, operand) + } + + override fun validateChildren(children: List): Boolean { + return children.size >= operandIndex + 1 && + operandIndex < children.size && children[operandIndex] is ASTNode + } + + override fun getBuilderName(): String = "UnaryOp($operator)" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/VariableBuilder.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/VariableBuilder.kt new file mode 100644 index 00000000..eb420f53 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/factory/builders/VariableBuilder.kt @@ -0,0 +1,46 @@ +package hs.kr.entrydsm.domain.ast.factory.builders + +import hs.kr.entrydsm.domain.ast.entities.VariableNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ๋ณ€์ˆ˜ ๋นŒ๋” - ๋ณ€์ˆ˜ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * ํ† ํฐ์˜ ๊ฐ’์„ ๋ณ€์ˆ˜๋ช…์œผ๋กœ ์‚ฌ์šฉํ•˜์—ฌ VariableNode๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +@Factory(context = "ast", complexity = Complexity.LOW, cache = false) +@Policy( + name = "Variable Naming Policy", + description = "๋ณ€์ˆ˜๋ช…์€ ์œ ํšจํ•œ ์‹๋ณ„์ž ํ˜•์‹์ด์–ด์•ผ ํ•˜๋ฉฐ ์˜ˆ์•ฝ์–ด๊ฐ€ ์•„๋‹ˆ์–ด์•ผ ํ•จ", + domain = "ast", + scope = Scope.ENTITY +) +object VariableBuilder : ASTBuilderContract { + override fun build(children: List): VariableNode { + if (children.size != 1) { + throw ASTException.variableChildrenMismatch(1, children.size) + } + if (children[0] !is Token) { + throw ASTException.variableFirstNotToken(children[0]::class.simpleName) + } + + + val token = children[0] as Token + return VariableNode(token.value) + } + + override fun validateChildren(children: List): Boolean { + return children.size == 1 && children[0] is Token + } + + override fun getBuilderName(): String = "Variable" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/interfaces/ASTVisitor.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/interfaces/ASTVisitor.kt new file mode 100644 index 00000000..4170f6a8 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/interfaces/ASTVisitor.kt @@ -0,0 +1,88 @@ +package hs.kr.entrydsm.domain.ast.interfaces + +import hs.kr.entrydsm.domain.ast.entities.NumberNode +import hs.kr.entrydsm.domain.ast.entities.BooleanNode +import hs.kr.entrydsm.domain.ast.entities.VariableNode +import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode +import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.entities.IfNode +import hs.kr.entrydsm.domain.ast.entities.ArgumentsNode + +/** + * AST ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•˜๊ธฐ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค (Visitor ํŒจํ„ด). + * + * ๊ฐ ๋…ธ๋“œ ํƒ€์ž…์— ๋Œ€ํ•œ ๋ฐฉ๋ฌธ ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•˜๋ฉฐ, ๋‹ค์–‘ํ•œ AST ์ฒ˜๋ฆฌ ๋กœ์ง์„ + * ๋…ธ๋“œ ํด๋ž˜์Šค์™€ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +interface ASTVisitor { + + /** + * ์ˆซ์ž ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ์ˆซ์ž ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitNumber(node: NumberNode): T + + /** + * ๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitBoolean(node: BooleanNode): T + + /** + * ๋ณ€์ˆ˜ ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ๋ณ€์ˆ˜ ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitVariable(node: VariableNode): T + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitBinaryOp(node: BinaryOpNode): T + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitUnaryOp(node: UnaryOpNode): T + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitFunctionCall(node: FunctionCallNode): T + + /** + * ์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitIf(node: IfNode): T + + /** + * ์ธ์ˆ˜ ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ์ธ์ˆ˜ ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitArguments(node: ArgumentsNode): T +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/ASTValidationPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/ASTValidationPolicy.kt new file mode 100644 index 00000000..dccee9b2 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/ASTValidationPolicy.kt @@ -0,0 +1,430 @@ +package hs.kr.entrydsm.domain.ast.policies + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.utils.ASTValidationUtils +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.PolicyResult +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * AST ๋…ธ๋“œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * AST ๋…ธ๋“œ์˜ ์ƒ์„ฑ๊ณผ ์กฐ์ž‘์— ๋Œ€ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์ •์˜ํ•˜๊ณ  ๊ฒ€์ฆํ•˜๋ฉฐ, + * ๋„๋ฉ”์ธ ๋ฌด๊ฒฐ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Policy( + name = "AST ๋…ธ๋“œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ •์ฑ…", + description = "AST ๋…ธ๋“œ์˜ ์ƒ์„ฑ๊ณผ ์กฐ์ž‘์— ๋Œ€ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์ •์˜ํ•˜๊ณ  ๊ฒ€์ฆ", + domain = "ast", + scope = Scope.DOMAIN +) +class ASTValidationPolicy { + + object ErrorMessages { + // ์ˆซ์ž ๋…ธ๋“œ ๊ด€๋ จ ๋ฉ”์‹œ์ง€ + const val NUMBER_NOT_FINITE = "์ˆซ์ž ๊ฐ’์€ ์œ ํ•œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val NUMBER_IS_NAN = "์ˆซ์ž ๊ฐ’์€ NaN์ด ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val NUMBER_EXCEEDS_MAX = "์ˆซ์ž ๊ฐ’์ด ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + const val NUMBER_BELOW_MIN = "์ˆซ์ž ๊ฐ’์ด ์ตœ์†Œ๊ฐ’์„ ๋ฏธ๋งŒ์ž…๋‹ˆ๋‹ค" + + // ๋ณ€์ˆ˜ ๋…ธ๋“œ ๊ด€๋ จ ๋ฉ”์‹œ์ง€ + const val VARIABLE_NAME_BLANK = "๋ณ€์ˆ˜๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val VARIABLE_NAME_TOO_LONG = "๋ณ€์ˆ˜๋ช…์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + const val VARIABLE_NAME_INVALID = "์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช…์ž…๋‹ˆ๋‹ค" + const val VARIABLE_NAME_RESERVED = "์˜ˆ์•ฝ์–ด๋Š” ๋ณ€์ˆ˜๋ช…์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + + // ์—ฐ์‚ฐ์ž ๊ด€๋ จ ๋ฉ”์‹œ์ง€ + const val OPERATOR_BLANK = "์—ฐ์‚ฐ์ž๋Š” ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val BINARY_OPERATOR_UNSUPPORTED = "์ง€์›๋˜์ง€ ์•Š๋Š” ์ดํ•ญ ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค" + const val UNARY_OPERATOR_UNSUPPORTED = "์ง€์›๋˜์ง€ ์•Š๋Š” ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค" + const val DIVISION_BY_ZERO = "0์œผ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val MODULO_BY_ZERO = "0์œผ๋กœ ๋‚˜๋ˆˆ ๋‚˜๋จธ์ง€๋ฅผ ๊ตฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + + // ํ”ผ์—ฐ์‚ฐ์ž ๊ด€๋ จ ๋ฉ”์‹œ์ง€ + const val LEFT_OPERAND_INVALID = "์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val RIGHT_OPERAND_INVALID = "์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val OPERAND_INVALID = "ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + + // ํ•จ์ˆ˜ ๊ด€๋ จ ๋ฉ”์‹œ์ง€ + const val FUNCTION_NAME_BLANK = "ํ•จ์ˆ˜๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val FUNCTION_NAME_TOO_LONG = "ํ•จ์ˆ˜๋ช…์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + const val FUNCTION_NAME_INVALID = "์œ ํšจํ•˜์ง€ ์•Š์€ ํ•จ์ˆ˜๋ช…์ž…๋‹ˆ๋‹ค" + const val FUNCTION_ARGS_EXCEED_MAX = "ํ•จ์ˆ˜ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + const val FUNCTION_ARG_INVALID = "์ธ์ˆ˜๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + + // ์กฐ๊ฑด๋ฌธ ๊ด€๋ จ ๋ฉ”์‹œ์ง€ + const val CONDITION_INVALID = "์กฐ๊ฑด์‹์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val TRUE_VALUE_INVALID = "์ฐธ ๊ฐ’์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val FALSE_VALUE_INVALID = "๊ฑฐ์ง“ ๊ฐ’์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val NESTING_DEPTH_EXCEEDED = "์ค‘์ฒฉ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + + // ์ธ์ˆ˜ ๊ด€๋ จ ๋ฉ”์‹œ์ง€ + const val ARGUMENTS_COUNT_EXCEEDED = "์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + const val ARGUMENT_INVALID = "์ธ์ˆ˜๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + + // ๋…ธ๋“œ ์ผ๋ฐ˜ ๊ฒ€์ฆ ๋ฉ”์‹œ์ง€ + const val NODE_SIZE_EXCEEDED = "๋…ธ๋“œ ํฌ๊ธฐ๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + const val NODE_DEPTH_EXCEEDED = "๋…ธ๋“œ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + const val VARIABLES_PER_NODE_EXCEEDED = "๋…ธ๋“œ๋‹น ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + } + + /** + * ์ˆซ์ž ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ˆซ์ž ๊ฐ’ + * @return ์ •์ฑ… ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateNumberCreation(value: Double): PolicyResult { + val violations = mutableListOf() + + if (!value.isFinite()) { + violations.add("${ErrorMessages.NUMBER_NOT_FINITE}: $value") + } + + if (value.isNaN()) { + violations.add(ErrorMessages.NUMBER_IS_NAN) + } + + // ๋„ˆ๋ฌด ํฐ ๊ฐ’ ๊ฒ€์ฆ + if (value > MAX_NUMBER_VALUE) { + violations.add("${ErrorMessages.NUMBER_EXCEEDS_MAX}: $value > $MAX_NUMBER_VALUE") + } + + if (value < MIN_NUMBER_VALUE) { + violations.add("${ErrorMessages.NUMBER_BELOW_MIN}: $value < $MIN_NUMBER_VALUE") + } + + return PolicyResult( + success = violations.isEmpty(), + message = violations.joinToString("; "), + data = mapOf("policyName" to NUMBER_NODE_POLICY) + ) + } + + /** + * ๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ถˆ๋ฆฌ์–ธ ๊ฐ’ + * @return ์ •์ฑ… ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateBooleanCreation(value: Boolean): PolicyResult { + // ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์€ ํ•ญ์ƒ ์œ ํšจ + return PolicyResult( + success = true, + message = "", + data = mapOf("policyName" to BOOLEAN_NODE_POLICY) + ) + } + + /** + * ๋ณ€์ˆ˜ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜๋ช… + * @return ์ •์ฑ… ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateVariableCreation(name: String): PolicyResult { + val violations = mutableListOf() + + if (name.isBlank()) { + violations.add(ErrorMessages.VARIABLE_NAME_BLANK) + } + + if (name.length > MAX_VARIABLE_NAME_LENGTH) { + violations.add("${ErrorMessages.VARIABLE_NAME_TOO_LONG}: ${name.length} > $MAX_VARIABLE_NAME_LENGTH") + } + + if (!isValidVariableName(name)) { + violations.add("${ErrorMessages.VARIABLE_NAME_INVALID}: $name") + } + + if (isReservedWord(name)) { + violations.add("${ErrorMessages.VARIABLE_NAME_RESERVED}: $name") + } + + return PolicyResult( + success = violations.isEmpty(), + message = violations.joinToString("; "), + data = mapOf("policyName" to VARIABLE_NODE_POLICY) + ) + } + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @param operator ์—ฐ์‚ฐ์ž + * @param right ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @return ์ •์ฑ… ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateBinaryOpCreation(left: ASTNode, operator: String, right: ASTNode): PolicyResult { + val violations = mutableListOf() + + if (operator.isBlank()) { + violations.add(ErrorMessages.OPERATOR_BLANK) + } + + if (!isSupportedBinaryOperator(operator)) { + violations.add("${ErrorMessages.BINARY_OPERATOR_UNSUPPORTED}: $operator") + } + + // ํ”ผ์—ฐ์‚ฐ์ž ๊ฒ€์ฆ + val leftValidation = validateNode(left) + if (!leftValidation.success) { + violations.add("${ErrorMessages.LEFT_OPERAND_INVALID}: ${leftValidation.message}") + } + + val rightValidation = validateNode(right) + if (!rightValidation.success) { + violations.add("${ErrorMessages.RIGHT_OPERAND_INVALID}: ${rightValidation.message}") + } + + // ์—ฐ์‚ฐ์ž๋ณ„ ํŠน๋ณ„ ๊ฒ€์ฆ + when (operator) { + "/" -> { + if (isZeroConstant(right)) { + violations.add(ErrorMessages.DIVISION_BY_ZERO) + } + } + "%" -> { + if (isZeroConstant(right)) { + violations.add(ErrorMessages.MODULO_BY_ZERO) + } + } + } + + return PolicyResult( + success = violations.isEmpty(), + message = violations.joinToString("; "), + data = mapOf("policyName" to BINARY_OPERATION_NODE_POLICY) + ) + } + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž + * @param operand ํ”ผ์—ฐ์‚ฐ์ž + * @return ์ •์ฑ… ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateUnaryOpCreation(operator: String, operand: ASTNode): PolicyResult { + val violations = mutableListOf() + + if (operator.isBlank()) { + violations.add(ErrorMessages.OPERATOR_BLANK) + } + + if (!isSupportedUnaryOperator(operator)) { + violations.add("${ErrorMessages.UNARY_OPERATOR_UNSUPPORTED}: $operator") + } + + // ํ”ผ์—ฐ์‚ฐ์ž ๊ฒ€์ฆ + val operandValidation = validateNode(operand) + if (!operandValidation.success) { + violations.add("${ErrorMessages.OPERAND_INVALID}: ${operandValidation.message}") + } + + return PolicyResult( + success = violations.isEmpty(), + message = violations.joinToString("; "), + data = mapOf("policyName" to UNARY_OPERATION_NODE_POLICY) + ) + } + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @param args ์ธ์ˆ˜ ๋ชฉ๋ก + * @return ์ •์ฑ… ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateFunctionCallCreation(name: String, args: List): PolicyResult { + val violations = mutableListOf() + + if (name.isBlank()) { + violations.add(ErrorMessages.FUNCTION_NAME_BLANK) + } + + if (name.length > MAX_FUNCTION_NAME_LENGTH) { + violations.add("${ErrorMessages.FUNCTION_NAME_TOO_LONG}: ${name.length} > $MAX_FUNCTION_NAME_LENGTH") + } + + if (!isValidFunctionName(name)) { + violations.add("${ErrorMessages.FUNCTION_NAME_INVALID}: $name") + } + + if (args.size > MAX_FUNCTION_ARGS) { + violations.add("${ErrorMessages.FUNCTION_ARGS_EXCEED_MAX}: ${args.size} > $MAX_FUNCTION_ARGS") + } + + // ๊ฐ ์ธ์ˆ˜ ๊ฒ€์ฆ + args.forEachIndexed { index, arg -> + val argValidation = validateNode(arg) + if (!argValidation.success) { + violations.add("${ErrorMessages.FUNCTION_ARG_INVALID} $index: ${argValidation.message}") + } + } + + return PolicyResult( + success = violations.isEmpty(), + message = violations.joinToString("; "), + data = mapOf("policyName" to FUNCTION_CALL_NODE_POLICY) + ) + } + + /** + * ์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param condition ์กฐ๊ฑด์‹ + * @param trueValue ์ฐธ ๊ฐ’ + * @param falseValue ๊ฑฐ์ง“ ๊ฐ’ + * @return ์ •์ฑ… ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateIfCreation(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode): PolicyResult { + val violations = mutableListOf() + + // ์กฐ๊ฑด์‹ ๊ฒ€์ฆ + val conditionValidation = validateNode(condition) + if (!conditionValidation.success) { + violations.add("${ErrorMessages.CONDITION_INVALID}: ${conditionValidation.message}") + } + + // ์ฐธ ๊ฐ’ ๊ฒ€์ฆ + val trueValidation = validateNode(trueValue) + if (!trueValidation.success) { + violations.add("${ErrorMessages.TRUE_VALUE_INVALID}: ${trueValidation.message}") + } + + // ๊ฑฐ์ง“ ๊ฐ’ ๊ฒ€์ฆ + val falseValidation = validateNode(falseValue) + if (!falseValidation.success) { + violations.add("${ErrorMessages.FALSE_VALUE_INVALID}: ${falseValidation.message}") + } + + // ์ค‘์ฒฉ ๊นŠ์ด ๊ฒ€์ฆ + val nestingDepth = calculateIfNodeNestingDepth(condition) + + calculateIfNodeNestingDepth(trueValue) + + calculateIfNodeNestingDepth(falseValue) + if (nestingDepth > MAX_NESTING_DEPTH) { + violations.add("${ErrorMessages.NESTING_DEPTH_EXCEEDED}: $nestingDepth > $MAX_NESTING_DEPTH") + } + + return PolicyResult( + success = violations.isEmpty(), + message = violations.joinToString("; "), + data = mapOf("policyName" to IF_NODE_POLICY) + ) + } + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param arguments ์ธ์ˆ˜ ๋ชฉ๋ก + * @return ์ •์ฑ… ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateArgumentsCreation(arguments: List): PolicyResult { + val violations = mutableListOf() + + if (arguments.size > MAX_ARGUMENTS_COUNT) { + violations.add("${ErrorMessages.ARGUMENTS_COUNT_EXCEEDED}: ${arguments.size} > $MAX_ARGUMENTS_COUNT") + } + + // ๊ฐ ์ธ์ˆ˜ ๊ฒ€์ฆ + arguments.forEachIndexed { index, arg -> + val argValidation = validateNode(arg) + if (!argValidation.success) { + violations.add("${ErrorMessages.ARGUMENT_INVALID} $index: ${argValidation.message}") + } + } + + return PolicyResult( + success = violations.isEmpty(), + message = violations.joinToString("; "), + data = mapOf("policyName" to ARGUMENT_LIST_POLICY) + ) + } + + /** + * ๋…ธ๋“œ ์ผ๋ฐ˜ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  ๋…ธ๋“œ + * @return ์ •์ฑ… ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateNode(node: ASTNode): PolicyResult { + val violations = mutableListOf() + + // ๋…ธ๋“œ ํฌ๊ธฐ ๊ฒ€์ฆ + if (node.getSize() > MAX_NODE_SIZE) { + violations.add("${ErrorMessages.NODE_SIZE_EXCEEDED}: ${node.getSize()} > $MAX_NODE_SIZE") + } + + // ๋…ธ๋“œ ๊นŠ์ด ๊ฒ€์ฆ + if (node.getDepth() > MAX_NODE_DEPTH) { + violations.add("${ErrorMessages.NODE_DEPTH_EXCEEDED}: ${node.getDepth()} > $MAX_NODE_DEPTH") + } + + // ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ ๊ฒ€์ฆ + if (node.getVariables().size > MAX_VARIABLES_PER_NODE) { + violations.add("${ErrorMessages.VARIABLES_PER_NODE_EXCEEDED}: ${node.getVariables().size} > $MAX_VARIABLES_PER_NODE") + } + + return PolicyResult( + success = violations.isEmpty(), + message = violations.joinToString("; "), + data = mapOf("policyName" to NODE_GENERAL_VERIFICATION_POLICY) + ) + } + + // ์ค‘๋ณต ๋ฉ”์„œ๋“œ๋“ค์„ ASTValidationUtils๋กœ ๋Œ€์ฒด + private fun isValidVariableName(name: String): Boolean = ASTValidationUtils.isValidVariableName(name) + private fun isValidFunctionName(name: String): Boolean = ASTValidationUtils.isValidFunctionName(name) + private fun isReservedWord(name: String): Boolean = ASTValidationUtils.isReservedWord(name) + private fun isSupportedBinaryOperator(operator: String): Boolean = ASTValidationUtils.isSupportedBinaryOperator(operator) + private fun isSupportedUnaryOperator(operator: String): Boolean = ASTValidationUtils.isSupportedUnaryOperator(operator) + private fun isZeroConstant(node: ASTNode): Boolean = ASTValidationUtils.isZeroConstant(node) + + /** + * IfNode์˜ ์ค‘์ฒฉ ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * ๋‹ค๋ฅธ ๋…ธ๋“œ ํƒ€์ž…์˜ ๊ฒฝ์šฐ 0์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun calculateIfNodeNestingDepth(node: ASTNode): Int { + return when (node) { + is hs.kr.entrydsm.domain.ast.entities.IfNode -> 1 + maxOf( + calculateIfNodeNestingDepth(node.condition), + calculateIfNodeNestingDepth(node.trueValue), + calculateIfNodeNestingDepth(node.falseValue) + ) + else -> 0 + } + } + + companion object { + private const val MAX_NUMBER_VALUE = 1e15 + private const val MIN_NUMBER_VALUE = -1e15 + private const val MAX_VARIABLE_NAME_LENGTH = 50 + private const val MAX_FUNCTION_NAME_LENGTH = 50 + private const val MAX_FUNCTION_ARGS = 10 + private const val MAX_ARGUMENTS_COUNT = 100 + private const val MAX_NODE_SIZE = 1000 + private const val MAX_NODE_DEPTH = 50 + private const val MAX_VARIABLES_PER_NODE = 100 + private const val MAX_NESTING_DEPTH = 20 + + private const val NUMBER_NODE_POLICY = "์ˆซ์ž ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…" + private const val BOOLEAN_NODE_POLICY = "๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…" + private const val VARIABLE_NODE_POLICY = "๋ณ€์ˆ˜ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…" + private const val BINARY_OPERATION_NODE_POLICY = "์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…" + private const val UNARY_OPERATION_NODE_POLICY = "๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…" + private const val FUNCTION_CALL_NODE_POLICY = "ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…" + private const val IF_NODE_POLICY = "์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…" + private const val ARGUMENT_LIST_POLICY = "์ธ์ˆ˜ ๋ชฉ๋ก ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…" + private const val NODE_GENERAL_VERIFICATION_POLICY = "๋…ธ๋“œ ์ผ๋ฐ˜ ๊ฒ€์ฆ ์ •์ฑ…" + + // ์ค‘๋ณต ์ƒ์ˆ˜๋“ค์„ ASTValidationUtils๋กœ ๋Œ€์ฒด + // RESERVED_WORDS, BINARY_OPERATORS, UNARY_OPERATORS๋Š” ASTValidationUtils์—์„œ ๊ด€๋ฆฌ + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/NodeCreationPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/NodeCreationPolicy.kt new file mode 100644 index 00000000..41b75ad1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/NodeCreationPolicy.kt @@ -0,0 +1,592 @@ +package hs.kr.entrydsm.domain.ast.policies + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.utils.ASTValidationUtils +import hs.kr.entrydsm.domain.ast.utils.FunctionValidationRules +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import hs.kr.entrydsm.domain.ast.policies.validation.* +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.PolicyResult +import hs.kr.entrydsm.global.annotation.policy.type.Scope +import java.util.concurrent.atomic.AtomicLong + +/** + * AST ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋…ธ๋“œ ์ƒ์„ฑ ์‹œ ์ ์šฉ๋˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™๊ณผ ์ œ์•ฝ์‚ฌํ•ญ์„ ์ •์˜ํ•˜๋ฉฐ, + * ์ƒ์„ฑ ์ „ ๊ฒ€์ฆ๊ณผ ์ƒ์„ฑ ํ›„ ์ตœ์ ํ™” ๊ทœ์น™์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @see = mapOf( + OPERATOR_DIVISION to DivisionValidationStrategy(), + OPERATOR_MODULO to ModuloValidationStrategy(), + OPERATOR_POWER to PowerValidationStrategy(), + OPERATOR_MULTIPLICATION to MultiplicationValidationStrategy(), + OPERATOR_ADDITION to DefaultValidationStrategy(OPERATOR_ADDITION), + OPERATOR_SUBTRACTION to DefaultValidationStrategy(OPERATOR_SUBTRACTION) + ) + + /** + * ์ˆซ์ž ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ˆซ์ž ๊ฐ’ + */ + fun validateNumberCreation(value: Double) { + if (!value.isFinite()) { + throw ASTException.numberNotFinite(value) + } + if (value.isNaN()) { + throw ASTException.numberIsNaN(value) + } + if (value < MIN_NUMBER_VALUE) { + throw ASTException.numberTooSmall(value, MIN_NUMBER_VALUE) + } + if (value > MAX_NUMBER_VALUE) { + throw ASTException.numberTooLarge(value, MAX_NUMBER_VALUE) + } + } + + /** + * ๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ถˆ๋ฆฌ์–ธ ๊ฐ’ + */ + fun validateBooleanCreation(value: Boolean) { + // ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์€ ํ•ญ์ƒ ์œ ํšจ + } + + /** + * ๋ณ€์ˆ˜ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜๋ช… + */ + fun validateVariableCreation(name: String) { + if (name.isBlank()) { + throw ASTException.variableNameEmpty() + } + if (name.length > MAX_VARIABLE_NAME_LENGTH) { + throw ASTException.variableNameTooLong(name.length, MAX_VARIABLE_NAME_LENGTH) + } + if (!isValidVariableName(name)) { + throw ASTException.invalidVariableName(name) + } + if (isReservedWord(name)) { + throw ASTException.variableReservedWord(name) + } + + // ๋ณ€์ˆ˜๋ช… ํŒจํ„ด ๊ฒ€์ฆ (์˜ต์…˜) + if (ENFORCE_NAMING_CONVENTION && !isValidNamingConvention(name)) { + throw ASTException.nodeValidationFailed( + reason = "${ErrorMessages.NAMING_CONVENTION_VIOLATION}: $name" + ) + } + } + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @param operator ์—ฐ์‚ฐ์ž + * @param right ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž + */ + fun validateBinaryOpCreation(left: ASTNode, operator: String, right: ASTNode) { + if (operator.isBlank()) { + throw ASTException.operatorEmpty() + } + if (!isSupportedBinaryOperator(operator)) { + throw ASTException.unsupportedBinaryOperator(operator) + } + + // ํ”ผ์—ฐ์‚ฐ์ž ๊ฒ€์ฆ + validateNodeForOperation(left, NodeContextMessages.LEFT_OPERAND) + validateNodeForOperation(right, NodeContextMessages.RIGHT_OPERAND) + + // ์—ฐ์‚ฐ์ž๋ณ„ ํŠน๋ณ„ ๊ฒ€์ฆ - Strategy ํŒจํ„ด ์ ์šฉ + val strategy = validationStrategies[operator] + if (strategy != null) { + strategy.validate(left, right, zeroConstantOptimizationCount) + } + + // ์ถ”๊ฐ€ ๊ณ ๊ธ‰ ์ตœ์ ํ™” ๋กœ์ง (๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž, ๋น„๊ต ์—ฐ์‚ฐ์ž ๋“ฑ) + when (operator) { + OPERATOR_LOGICAL_AND -> { + if (isTrueConstant(left) || isFalseConstant(left) || + isTrueConstant(right) || isFalseConstant(right) || + left.isStructurallyEqual(right) + ) { + constantConditionOptimizationCount.incrementAndGet() + } + } + OPERATOR_LOGICAL_OR -> { + if (isTrueConstant(left) || isFalseConstant(left) || + isTrueConstant(right) || isFalseConstant(right) || + left.isStructurallyEqual(right) + ) { + constantConditionOptimizationCount.incrementAndGet() + } + } + OPERATOR_EQUAL, OPERATOR_NOT_EQUAL -> { + if (left.isStructurallyEqual(right)) { + constantConditionOptimizationCount.incrementAndGet() + } + } + OPERATOR_LESS_THAN, OPERATOR_LESS_THAN_OR_EQUAL, OPERATOR_GREATER_THAN, OPERATOR_GREATER_THAN_OR_EQUAL -> { + if (left.isStructurallyEqual(right)) { + constantConditionOptimizationCount.incrementAndGet() + } + } + } + + // ์ˆœํ™˜ ์ฐธ์กฐ ๊ฒ€์ฆ(์˜ต์…˜) + if (PREVENT_CIRCULAR_REFERENCES && hasCircularReference(left, right)) { + circularReferenceDetectionCount.incrementAndGet() + throw ASTException.nodeValidationFailed( + reason = ErrorMessages.CIRCULAR_REFERENCE_DETECTED + ) + } + } + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž + * @param operand ํ”ผ์—ฐ์‚ฐ์ž + */ + fun validateUnaryOpCreation(operator: String, operand: ASTNode) { + if (operator.isBlank()) { + throw ASTException.operatorEmpty() + } + if (!isSupportedUnaryOperator(operator)) { + throw ASTException.unsupportedUnaryOperator(operator) + } + + // ํ”ผ์—ฐ์‚ฐ์ž ๊ฒ€์ฆ + validateNodeForOperation(operand, NodeContextMessages.OPERAND) + + // ์—ฐ์‚ฐ์ž๋ณ„ ํŠน๋ณ„ ๊ฒ€์ฆ ๋ฐ ์ตœ์ ํ™” ํžŒํŠธ + when (operator) { + OPERATOR_LOGICAL_NOT -> { + if (STRICT_LOGICAL_OPERATIONS && !isLogicalCompatible(operand)) { + throw ASTException.logicalIncompatibleOperand() + } + if (isTrueConstant(operand) || isFalseConstant(operand) || + (operand is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode && operand.isLogicalNot()) + ) { + constantConditionOptimizationCount.incrementAndGet() + } + } + OPERATOR_UNARY_MINUS -> { + if (isZeroConstant(operand)) { + zeroConstantOptimizationCount.incrementAndGet() // -0 = 0 + } else if (operand is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode && operand.isNegation()) { + zeroConstantOptimizationCount.incrementAndGet() // -(-x) = x + } else if (operand is hs.kr.entrydsm.domain.ast.entities.NumberNode && operand.value < 0) { + zeroConstantOptimizationCount.incrementAndGet() // -(์Œ์ˆ˜) = ์–‘์ˆ˜ + } + } + OPERATOR_UNARY_PLUS -> { + // +x = x + zeroConstantOptimizationCount.incrementAndGet() + } + } + } + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @param args ์ธ์ˆ˜ ๋ชฉ๋ก + */ + fun validateFunctionCallCreation(name: String, args: List) { + if (name.isBlank()) { + throw ASTException.functionNameEmpty() + } + if (name.length > MAX_FUNCTION_NAME_LENGTH) { + throw ASTException.functionNameTooLong(name.length, MAX_FUNCTION_NAME_LENGTH) + } + if (!isValidFunctionName(name)) { + throw ASTException.invalidFunctionName(name) + } + if (args.size > MAX_FUNCTION_ARGS) { + throw ASTException.functionArgumentsExceeded(args.size, MAX_FUNCTION_ARGS) + } + + // ๊ฐ ์ธ์ˆ˜ ๊ฒ€์ฆ + args.forEachIndexed { index, arg -> + validateNodeForOperation(arg, "${NodeContextMessages.ARGUMENT} $index") + } + + // ํ•จ์ˆ˜๋ณ„ ๊ทœ์น™ + validateFunctionSpecificRules(name, args) + } + + /** + * ์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param condition ์กฐ๊ฑด์‹ + * @param trueValue ์ฐธ ๊ฐ’ + * @param falseValue ๊ฑฐ์ง“ ๊ฐ’ + */ + fun validateIfCreation(condition: ASTNode, trueValue: ASTNode, falseValue: ASTNode) { + // ๊ฐ ๋…ธ๋“œ ๊ฒ€์ฆ + validateNodeForOperation(condition, NodeContextMessages.CONDITION) + validateNodeForOperation(trueValue, NodeContextMessages.TRUE_VALUE) + validateNodeForOperation(falseValue, NodeContextMessages.FALSE_VALUE) + + // ์ค‘์ฒฉ ๊นŠ์ด ๊ฒ€์ฆ + val totalDepth = condition.getDepth() + trueValue.getDepth() + falseValue.getDepth() + if (totalDepth > MAX_TOTAL_DEPTH) { + throw ASTException.ifTotalDepthExceeded(totalDepth, MAX_TOTAL_DEPTH) + } + + // ์กฐ๊ฑด๋ฌธ ํŠน๋ณ„ ๊ฒ€์ฆ(์ƒ์ˆ˜ ์กฐ๊ฑด ์ตœ์ ํ™” ๊ฐ์ง€) + if (OPTIMIZE_CONSTANT_CONDITIONS && condition.isLiteral()) { + when (condition) { + is hs.kr.entrydsm.domain.ast.entities.BooleanNode -> { + // ํ•ญ์ƒ ์ฐธ/๊ฑฐ์ง“ + constantConditionOptimizationCount.incrementAndGet() + } + is hs.kr.entrydsm.domain.ast.entities.NumberNode -> { + // 0/๋น„0 + constantConditionOptimizationCount.incrementAndGet() + } + else -> { /* no-op */ } + } + } + } + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก ๋…ธ๋“œ ์ƒ์„ฑ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param arguments ์ธ์ˆ˜ ๋ชฉ๋ก + */ + fun validateArgumentsCreation(arguments: List) { + if (arguments.size > MAX_ARGUMENTS_COUNT) { + throw ASTException.argumentsExceeded(arguments.size, MAX_ARGUMENTS_COUNT) + } + + // ๊ฐ ์ธ์ˆ˜ ๊ฒ€์ฆ + arguments.forEachIndexed { index, arg -> + validateNodeForOperation(arg, "${NodeContextMessages.ARGUMENT} $index") + } + + // ์ธ์ˆ˜ ์ค‘๋ณต ๊ฒ€์ฆ(์˜ต์…˜) + if (PREVENT_DUPLICATE_ARGUMENTS) { + val duplicates = findDuplicateArguments(arguments) + if (duplicates.isNotEmpty()) { + throw ASTException.argumentsDuplicated(duplicates) + } + } + } + + /** + * ์—ฐ์‚ฐ์— ์‚ฌ์šฉ๋  ๋…ธ๋“œ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  ๋…ธ๋“œ + * @param context ์ปจํ…์ŠคํŠธ ์ •๋ณด + */ + private fun validateNodeForOperation(node: ASTNode, context: String) { + val size = node.getSize() + val depth = node.getDepth() + val vars = node.getVariables().size + + if (size > MAX_NODE_SIZE) { + throw ASTException.nodeSizeExceeded(size, MAX_NODE_SIZE, context) + } + if (depth > MAX_NODE_DEPTH) { + throw ASTException.nodeDepthExceeded(depth, MAX_NODE_DEPTH, context) + } + if (vars > MAX_VARIABLES_PER_NODE) { + throw ASTException.nodeVariablesExceeded(vars, MAX_VARIABLES_PER_NODE, context) + } + } + + // === ASTValidationUtils ์œ„์ž„ === + private fun isValidVariableName(name: String): Boolean = ASTValidationUtils.isValidVariableName(name) + private fun isValidFunctionName(name: String): Boolean = ASTValidationUtils.isValidFunctionName(name) + private fun isReservedWord(name: String): Boolean = ASTValidationUtils.isReservedWord(name) + private fun isSupportedBinaryOperator(operator: String): Boolean = ASTValidationUtils.isSupportedBinaryOperator(operator) + private fun isSupportedUnaryOperator(operator: String): Boolean = ASTValidationUtils.isSupportedUnaryOperator(operator) + private fun isZeroConstant(node: ASTNode): Boolean = ASTValidationUtils.isZeroConstant(node) + + /** + * ๋„ค์ด๋ฐ ๊ทœ์น™์„ ์ค€์ˆ˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidNamingConvention(name: String): Boolean { + // ์นด๋ฉœ ์ผ€์ด์Šค ๋˜๋Š” ์Šค๋„ค์ดํฌ ์ผ€์ด์Šค ํ—ˆ์šฉ + return name.matches(Regex(NAMING_CONVENTION_PATTERN)) + } + + /** + * ๋…ธ๋“œ๊ฐ€ 1 ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isOneConstant(node: ASTNode): Boolean { + return node is hs.kr.entrydsm.domain.ast.entities.NumberNode && node.value == 1.0 + } + + /** + * ๋…ธ๋“œ๊ฐ€ true ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isTrueConstant(node: ASTNode): Boolean { + return node is hs.kr.entrydsm.domain.ast.entities.BooleanNode && node.value + } + + /** + * ๋…ธ๋“œ๊ฐ€ false ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isFalseConstant(node: ASTNode): Boolean { + return node is hs.kr.entrydsm.domain.ast.entities.BooleanNode && !node.value + } + + /** + * ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์— ํ˜ธํ™˜๋˜๋Š” ๋…ธ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isLogicalCompatible(node: ASTNode): Boolean { + return when (node) { + is hs.kr.entrydsm.domain.ast.entities.BooleanNode -> true + is hs.kr.entrydsm.domain.ast.entities.NumberNode -> true + is hs.kr.entrydsm.domain.ast.entities.BinaryOpNode -> + node.isComparisonOperator() || node.isLogicalOperator() + is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode -> + node.isLogicalOperator() + else -> false + } + } + + /** + * ์ˆœํ™˜ ์ฐธ์กฐ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasCircularReference(left: ASTNode, right: ASTNode): Boolean { + // ์ง์ ‘์ ์ธ ๋™์ผ์„ฑ ๊ฒ€์‚ฌ + if (left === right) { + return true + } + + // ์ขŒ์ธก ๋…ธ๋“œ๊ฐ€ ์šฐ์ธก ๋…ธ๋“œ๋ฅผ ์ฐธ์กฐํ•˜๋Š”์ง€ ํ™•์ธ + if (containsNode(left, right)) { + return true + } + + // ์šฐ์ธก ๋…ธ๋“œ๊ฐ€ ์ขŒ์ธก ๋…ธ๋“œ๋ฅผ ์ฐธ์กฐํ•˜๋Š”์ง€ ํ™•์ธ + if (containsNode(right, left)) { + return true + } + + return false + } + + /** + * ์ฃผ์–ด์ง„ ๋…ธ๋“œ๊ฐ€ ๋‹ค๋ฅธ ๋…ธ๋“œ๋ฅผ ํฌํ•จํ•˜๋Š”์ง€ DFS๋กœ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun containsNode( + container: ASTNode, + target: ASTNode, + visited: MutableSet = mutableSetOf() + ): Boolean { + // ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ + if (container in visited) { + return false + } + + // ์ง์ ‘ ์ผ์น˜ + if (container === target) { + return true + } + + // ๋ฐฉ๋ฌธ ํ‘œ์‹œ + visited.add(container) + + // ์ž์‹ ๋…ธ๋“œ๋“ค์„ ์žฌ๊ท€์ ์œผ๋กœ ๊ฒ€์‚ฌ + val hasTarget = container.getChildren().any { child -> + containsNode(child, target, visited) + } + + // ๋ฐฉ๋ฌธ ํ‘œ์‹œ ํ•ด์ œ (๋ฐฑํŠธ๋ž˜ํ‚น) + visited.remove(container) + return hasTarget + } + + /** + * ๋…ธ๋“œ ํŠธ๋ฆฌ์—์„œ ์ˆœํ™˜ ์ฐธ์กฐ๋ฅผ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun detectCircularReferenceInTree(root: ASTNode): Boolean { + val visiting = mutableSetOf() // ํ˜„์žฌ ๋ฐฉ๋ฌธ ์ค‘์ธ ๋…ธ๋“œ๋“ค + val visited = mutableSetOf() // ์™„์ „ํžˆ ์ฒ˜๋ฆฌ๋œ ๋…ธ๋“œ๋“ค + + fun dfs(node: ASTNode): Boolean { + // ํ˜„์žฌ ๋ฐฉ๋ฌธ ์ค‘์ธ ๋…ธ๋“œ๋ฅผ ๋‹ค์‹œ ๋ฐฉ๋ฌธํ•˜๋ฉด ์ˆœํ™˜ ์ฐธ์กฐ + if (node in visiting) { + return true + } + + // ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ๋…ธ๋“œ๋Š” ๊ฑด๋„ˆ๋›ฐ๊ธฐ + if (node in visited) { + return false + } + + // ๋ฐฉ๋ฌธ ์‹œ์ž‘ + visiting.add(node) + for (child in node.getChildren()) { + if (dfs(child)) { + return true + } + } + + // ๋ฐฉ๋ฌธ ์™„๋ฃŒ + visiting.remove(node) + visited.add(node) + return false + } + return dfs(root) + } + + /** + * ์ค‘๋ณต ์ธ์ˆ˜๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + private fun findDuplicateArguments(arguments: List): List { + val seen = mutableSetOf() + val duplicates = mutableListOf() + arguments.forEach { arg -> + val argString = arg.toString() + if (argString in seen) { + duplicates.add(argString) + } else { + seen.add(argString) + } + } + + return duplicates + } + + /** + * ํ•จ์ˆ˜๋ณ„ ํŠน๋ณ„ ๊ทœ์น™์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateFunctionSpecificRules(name: String, args: List) { + if (!FunctionValidationRules.isValidFunctionCall(name, args)) { + val description = FunctionValidationRules.getArgumentCountDescription(name) + throw ASTException.functionArgumentCountMismatch( + name = name, + expectedDesc = description, + actual = args.size + ) + } + } + + companion object { + // ์ œ์•ฝ ์ƒ์ˆ˜ + private const val MAX_NUMBER_VALUE = 1e15 + private const val MIN_NUMBER_VALUE = -1e15 + private const val MAX_VARIABLE_NAME_LENGTH = 50 + private const val MAX_FUNCTION_NAME_LENGTH = 50 + private const val MAX_FUNCTION_ARGS = 10 + private const val MAX_ARGUMENTS_COUNT = 100 + private const val MAX_NODE_SIZE = 1000 + private const val MAX_NODE_DEPTH = 50 + private const val MAX_VARIABLES_PER_NODE = 100 + private const val MAX_TOTAL_DEPTH = 100 + + // ์ •์ฑ… ํ”Œ๋ž˜๊ทธ + private const val ENFORCE_NAMING_CONVENTION = true + private const val STRICT_LOGICAL_OPERATIONS = true + private const val PREVENT_CIRCULAR_REFERENCES = true + private const val OPTIMIZE_CONSTANT_CONDITIONS = true + private const val PREVENT_DUPLICATE_ARGUMENTS = false + + // ์—ฐ์‚ฐ์ž ์ƒ์ˆ˜ + private const val OPERATOR_DIVISION = "/" + private const val OPERATOR_MODULO = "%" + private const val OPERATOR_POWER = "^" + private const val OPERATOR_MULTIPLICATION = "*" + private const val OPERATOR_ADDITION = "+" + private const val OPERATOR_SUBTRACTION = "-" + private const val OPERATOR_LOGICAL_AND = "&&" + private const val OPERATOR_LOGICAL_OR = "||" + private const val OPERATOR_EQUAL = "==" + private const val OPERATOR_NOT_EQUAL = "!=" + private const val OPERATOR_LESS_THAN = "<" + private const val OPERATOR_LESS_THAN_OR_EQUAL = "<=" + private const val OPERATOR_GREATER_THAN = ">" + private const val OPERATOR_GREATER_THAN_OR_EQUAL = ">=" + private const val OPERATOR_LOGICAL_NOT = "!" + private const val OPERATOR_UNARY_MINUS = "-" + private const val OPERATOR_UNARY_PLUS = "+" + + // ํŒจํ„ด ์ƒ์ˆ˜ + private const val NAMING_CONVENTION_PATTERN = "^[a-z_][a-zA-Z0-9_]*$" + + // ์ตœ์ ํ™” ํ†ต๊ณ„ ์นด์šดํ„ฐ + private val constantConditionOptimizationCount = AtomicLong(0) + private val zeroConstantOptimizationCount = AtomicLong(0) + private val circularReferenceDetectionCount = AtomicLong(0) + + // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ƒ์ˆ˜ + object ErrorMessages { + const val NAMING_CONVENTION_VIOLATION = "๋„ค์ด๋ฐ ๊ทœ์น™ ์œ„๋ฐ˜" + const val CIRCULAR_REFERENCE_DETECTED = "์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + } + + // ๋…ธ๋“œ ์ปจํ…์ŠคํŠธ ๋ฉ”์‹œ์ง€ + object NodeContextMessages { + const val LEFT_OPERAND = "์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž" + const val RIGHT_OPERAND = "์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž" + const val OPERAND = "ํ”ผ์—ฐ์‚ฐ์ž" + const val ARGUMENT = "์ธ์ˆ˜" + const val CONDITION = "์กฐ๊ฑด์‹" + const val TRUE_VALUE = "์ฐธ ๊ฐ’" + const val FALSE_VALUE = "๊ฑฐ์ง“ ๊ฐ’" + } + + // ํ†ต๊ณ„ ํ‚ค ์ƒ์ˆ˜ + object StatisticsKeys { + const val CONSTANT_CONDITION_OPTIMIZATIONS = "constantConditionOptimizations" + const val ZERO_CONSTANT_OPTIMIZATIONS = "zeroConstantOptimizations" + const val CIRCULAR_REFERENCE_DETECTIONS = "circularReferenceDetections" + const val OPTIMIZATION_FLAGS = "optimizationFlags" + const val ENFORCE_NAMING_CONVENTION_FLAG = "enforceNamingConvention" + const val STRICT_LOGICAL_OPERATIONS_FLAG = "strictLogicalOperations" + const val PREVENT_CIRCULAR_REFERENCES_FLAG = "preventCircularReferences" + const val OPTIMIZE_CONSTANT_CONDITIONS_FLAG = "optimizeConstantConditions" + const val PREVENT_DUPLICATE_ARGUMENTS_FLAG = "preventDuplicateArguments" + } + + /** + * ์ •์ฑ… ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getPolicyStatistics(): Map { + return mapOf( + StatisticsKeys.CONSTANT_CONDITION_OPTIMIZATIONS to constantConditionOptimizationCount.get(), + StatisticsKeys.ZERO_CONSTANT_OPTIMIZATIONS to zeroConstantOptimizationCount.get(), + StatisticsKeys.CIRCULAR_REFERENCE_DETECTIONS to circularReferenceDetectionCount.get(), + StatisticsKeys.OPTIMIZATION_FLAGS to mapOf( + StatisticsKeys.ENFORCE_NAMING_CONVENTION_FLAG to ENFORCE_NAMING_CONVENTION, + StatisticsKeys.STRICT_LOGICAL_OPERATIONS_FLAG to STRICT_LOGICAL_OPERATIONS, + StatisticsKeys.PREVENT_CIRCULAR_REFERENCES_FLAG to PREVENT_CIRCULAR_REFERENCES, + StatisticsKeys.OPTIMIZE_CONSTANT_CONDITIONS_FLAG to OPTIMIZE_CONSTANT_CONDITIONS, + StatisticsKeys.PREVENT_DUPLICATE_ARGUMENTS_FLAG to PREVENT_DUPLICATE_ARGUMENTS + ) + ) + } + + /** + * ํ†ต๊ณ„ ์นด์šดํ„ฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun resetStatistics() { + constantConditionOptimizationCount.set(0) + zeroConstantOptimizationCount.set(0) + circularReferenceDetectionCount.set(0) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/BinaryOperatorValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/BinaryOperatorValidationStrategy.kt new file mode 100644 index 00000000..7bf21714 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/BinaryOperatorValidationStrategy.kt @@ -0,0 +1,48 @@ +package hs.kr.entrydsm.domain.ast.policies.validation + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import java.util.concurrent.atomic.AtomicLong + +/** + * ์ดํ•ญ ์—ฐ์‚ฐ์ž๋ณ„ ๊ฒ€์ฆ ์ „๋žต์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Strategy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์—ฐ์‚ฐ์ž๋ณ„๋กœ ์„œ๋กœ ๋‹ค๋ฅธ ๊ฒ€์ฆ ๋กœ์ง์„ + * ๋…๋ฆฝ์ ์œผ๋กœ ๊ตฌํ˜„ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.13 + */ +interface BinaryOperatorValidationStrategy { + + /** + * ์ด ์ „๋žต์ด ์ง€์›ํ•˜๋Š” ์—ฐ์‚ฐ์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun supportedOperator(): String + + /** + * ์—ฐ์‚ฐ์ž๋ณ„ ํŠน๋ณ„ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @param right ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž + * @param optimizationCounter ์ตœ์ ํ™” ์นด์šดํ„ฐ (ํ•„์š” ์‹œ ์‚ฌ์šฉ) + */ + fun validate( + left: ASTNode, + right: ASTNode, + optimizationCounter: AtomicLong + ) + + /** + * ๋…ธ๋“œ๊ฐ€ 0 ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isZeroConstant(node: ASTNode): Boolean { + return node.getNodeType() == "NumberNode" && node.toString() == "0" + } + + /** + * ๋…ธ๋“œ๊ฐ€ 1 ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isOneConstant(node: ASTNode): Boolean { + return node.getNodeType() == "NumberNode" && node.toString() == "1" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DefaultValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DefaultValidationStrategy.kt new file mode 100644 index 00000000..da471866 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DefaultValidationStrategy.kt @@ -0,0 +1,46 @@ +package hs.kr.entrydsm.domain.ast.policies.validation + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import java.util.concurrent.atomic.AtomicLong + +/** + * ๊ธฐ๋ณธ ์—ฐ์‚ฐ์ž (+, -) ๊ฒ€์ฆ ์ „๋žต์ž…๋‹ˆ๋‹ค. + * + * ํŠน๋ณ„ํ•œ ๊ฒ€์ฆ ๋กœ์ง์ด ์—†๋Š” ์—ฐ์‚ฐ์ž๋“ค์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์ „๋žต์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.13 + */ +class DefaultValidationStrategy(private val operator: String) : BinaryOperatorValidationStrategy { + + override fun supportedOperator(): String = operator + + override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) { + when (operator) { + "+" -> { + // 0๊ณผ์˜ ๋ง์…ˆ ์ตœ์ ํ™” ๊ฐ์ง€ (x + 0 = x, 0 + x = x) + if (isZeroConstant(left) || isZeroConstant(right)) { + optimizationCounter.incrementAndGet() + } + // ๊ฐ™์€ ํ”ผ์—ฐ์‚ฐ์ž ์ตœ์ ํ™” ๊ฐ์ง€ (x + x = 2*x) + if (left.isStructurallyEqual(right)) { + optimizationCounter.incrementAndGet() + } + } + "-" -> { + // 0๊ณผ์˜ ๋บ„์…ˆ ์ตœ์ ํ™” ๊ฐ์ง€ (x - 0 = x) + if (isZeroConstant(right)) { + optimizationCounter.incrementAndGet() + } + // 0์—์„œ ๋นผ๊ธฐ ์ตœ์ ํ™” ๊ฐ์ง€ (0 - x = -x) + if (isZeroConstant(left)) { + optimizationCounter.incrementAndGet() + } + // ๊ฐ™์€ ํ”ผ์—ฐ์‚ฐ์ž ์ตœ์ ํ™” ๊ฐ์ง€ (x - x = 0) + if (left.isStructurallyEqual(right)) { + optimizationCounter.incrementAndGet() + } + } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DivisionValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DivisionValidationStrategy.kt new file mode 100644 index 00000000..7b4cbefb --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/DivisionValidationStrategy.kt @@ -0,0 +1,25 @@ +package hs.kr.entrydsm.domain.ast.policies.validation + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import java.util.concurrent.atomic.AtomicLong + +/** + * ๋‚˜๋ˆ—์…ˆ ์—ฐ์‚ฐ์ž (/) ๊ฒ€์ฆ ์ „๋žต์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.13 + */ +class DivisionValidationStrategy : BinaryOperatorValidationStrategy { + + override fun supportedOperator(): String = "/" + + override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) { + if (isZeroConstant(right)) { + throw ASTException.divisionByZero() + } + if (isOneConstant(right)) { + optimizationCounter.incrementAndGet() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/ModuloValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/ModuloValidationStrategy.kt new file mode 100644 index 00000000..c867ce42 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/ModuloValidationStrategy.kt @@ -0,0 +1,25 @@ +package hs.kr.entrydsm.domain.ast.policies.validation + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import java.util.concurrent.atomic.AtomicLong + +/** + * ๋‚˜๋จธ์ง€ ์—ฐ์‚ฐ์ž (%) ๊ฒ€์ฆ ์ „๋žต์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.13 + */ +class ModuloValidationStrategy : BinaryOperatorValidationStrategy { + + override fun supportedOperator(): String = "%" + + override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) { + if (isZeroConstant(right)) { + throw ASTException.moduloByZero() + } + if (isOneConstant(right)) { + optimizationCounter.incrementAndGet() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/MultiplicationValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/MultiplicationValidationStrategy.kt new file mode 100644 index 00000000..668e0ce3 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/MultiplicationValidationStrategy.kt @@ -0,0 +1,26 @@ +package hs.kr.entrydsm.domain.ast.policies.validation + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import java.util.concurrent.atomic.AtomicLong + +/** + * ๊ณฑ์…ˆ ์—ฐ์‚ฐ์ž (*) ๊ฒ€์ฆ ์ „๋žต์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.13 + */ +class MultiplicationValidationStrategy : BinaryOperatorValidationStrategy { + + override fun supportedOperator(): String = "*" + + override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) { + // 0๊ณผ์˜ ๊ณฑ์…ˆ ์ตœ์ ํ™” ๊ฐ์ง€ (x * 0 = 0, 0 * x = 0) + if (isZeroConstant(left) || isZeroConstant(right)) { + optimizationCounter.incrementAndGet() + } + // 1๊ณผ์˜ ๊ณฑ์…ˆ ์ตœ์ ํ™” ๊ฐ์ง€ (x * 1 = x, 1 * x = x) + else if (isOneConstant(left) || isOneConstant(right)) { + optimizationCounter.incrementAndGet() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/PowerValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/PowerValidationStrategy.kt new file mode 100644 index 00000000..7895f754 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/policies/validation/PowerValidationStrategy.kt @@ -0,0 +1,39 @@ +package hs.kr.entrydsm.domain.ast.policies.validation + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException +import java.util.concurrent.atomic.AtomicLong + +/** + * ๊ฑฐ๋“ญ์ œ๊ณฑ ์—ฐ์‚ฐ์ž (^) ๊ฒ€์ฆ ์ „๋žต์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.13 + */ +class PowerValidationStrategy : BinaryOperatorValidationStrategy { + + override fun supportedOperator(): String = "^" + + override fun validate(left: ASTNode, right: ASTNode, optimizationCounter: AtomicLong) { + if (isZeroConstant(left) && isZeroConstant(right)) { + optimizationCounter.incrementAndGet() + throw ASTException.zeroPowerZero() + } + + // ๊ฑฐ๋“ญ์ œ๊ณฑ ์ตœ์ ํ™” ๊ฐ์ง€ + when { + isOneConstant(left) -> { + // 1^x = 1 + optimizationCounter.incrementAndGet() + } + isZeroConstant(right) -> { + // x^0 = 1 (x != 0) + optimizationCounter.incrementAndGet() + } + isOneConstant(right) -> { + // x^1 = x + optimizationCounter.incrementAndGet() + } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeOptimizer.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeOptimizer.kt new file mode 100644 index 00000000..f7888ae4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeOptimizer.kt @@ -0,0 +1,690 @@ +package hs.kr.entrydsm.domain.ast.services + +import hs.kr.entrydsm.domain.ast.entities.* +import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.entities.IfNode +import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode +import hs.kr.entrydsm.domain.ast.factories.ASTNodeFactory +import hs.kr.entrydsm.domain.ast.values.NodeSize +import hs.kr.entrydsm.domain.ast.values.TreeDepth +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.global.configuration.ASTConfiguration +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider +import kotlin.math.* + +/** + * AST ํŠธ๋ฆฌ๋ฅผ ์ตœ์ ํ™”ํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * ์ƒ์ˆ˜ ํด๋”ฉ, ๊ณตํ†ต ํ•˜์œ„ ํ‘œํ˜„์‹ ์ œ๊ฑฐ, ๋ถˆํ•„์š”ํ•œ ๋…ธ๋“œ ์ œ๊ฑฐ ๋“ฑ์˜ + * ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์ ์šฉํ•˜์—ฌ ํŠธ๋ฆฌ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Service( + name = "AST ํŠธ๋ฆฌ ์ตœ์ ํ™” ์„œ๋น„์Šค", + type = ServiceType.DOMAIN_SERVICE +) +class TreeOptimizer( + private val configurationProvider: ConfigurationProvider? = null +) { + + private val factory = ASTNodeFactory() + private val traverser = TreeTraverser() + + // ์„ค์ •์€ ConfigurationProvider๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ์ ‘๊ทผ (๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ ๊ฐ€๋Šฅ) + private val config: ASTConfiguration + get() = configurationProvider?.getASTConfiguration() ?: ASTConfiguration() + + /** + * ํŠธ๋ฆฌ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param root ์ตœ์ ํ™”ํ•  ๋ฃจํŠธ ๋…ธ๋“œ + * @return ์ตœ์ ํ™”๋œ ํŠธ๋ฆฌ + */ + fun optimize(root: ASTNode): ASTNode { + var optimized = root + + for (pass in 1..5) { + val beforeSize = optimized.getSize() + + optimized = performOptimizationPass(optimized) + + val afterSize = optimized.getSize() + + if (beforeSize == afterSize) { + break + } + } + + return optimized + } + + /** + * ๋‹จ์ผ ์ตœ์ ํ™” ํŒจ์Šค๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performOptimizationPass(root: ASTNode): ASTNode { + var optimized = root + + // 1. ์ƒ์ˆ˜ ํด๋”ฉ + optimized = constantFolding(optimized) + + // 2. ํ•ญ๋“ฑ ์›์†Œ ์ œ๊ฑฐ + optimized = eliminateIdentityElements(optimized) + + // 3. ๋ถˆํ•„์š”ํ•œ ์กฐ๊ฑด๋ฌธ ์ œ๊ฑฐ + optimized = eliminateUnnecessaryConditionals(optimized) + + // 4. ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋‹จ์ˆœํ™” + optimized = simplifyUnaryOperations(optimized) + + // 5. ์ค‘๋ณต ๋…ธ๋“œ ์ œ๊ฑฐ + optimized = eliminateDuplicateNodes(optimized) + + // 6. ๊ณตํ†ต ํ•˜์œ„ ํ‘œํ˜„์‹ ์ œ๊ฑฐ + optimized = eliminateCommonSubexpressions(optimized) + + return optimized + } + + /** + * ์ƒ์ˆ˜ ํด๋”ฉ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun constantFolding(root: ASTNode): ASTNode { + return when (root) { + is BinaryOpNode -> { + val leftOptimized = constantFolding(root.left) + val rightOptimized = constantFolding(root.right) + + // ์–‘์ชฝ ๋ชจ๋‘ ์ƒ์ˆ˜์ธ ๊ฒฝ์šฐ ๊ณ„์‚ฐ + if (leftOptimized is NumberNode && rightOptimized is NumberNode) { + evaluateConstantBinaryOp(leftOptimized, root.operator, rightOptimized) + } else { + factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + } + + is UnaryOpNode -> { + val operandOptimized = constantFolding(root.operand) + + // ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์ƒ์ˆ˜์ธ ๊ฒฝ์šฐ ๊ณ„์‚ฐ + if (operandOptimized is NumberNode) { + evaluateConstantUnaryOp(root.operator, operandOptimized) + } else { + factory.createUnaryOp(root.operator, operandOptimized) + } + } + + is FunctionCallNode -> { + val optimizedArgs = root.args.map { constantFolding(it) } + + // ๋ชจ๋“  ์ธ์ˆ˜๊ฐ€ ์ƒ์ˆ˜์ธ ๊ฒฝ์šฐ ๊ณ„์‚ฐ + if (optimizedArgs.all { it is NumberNode }) { + evaluateConstantFunctionCall(root.name, optimizedArgs.map { it as NumberNode }) + } else { + factory.createFunctionCall(root.name, optimizedArgs) + } + } + + is IfNode -> { + val conditionOptimized = constantFolding(root.condition) + val trueOptimized = constantFolding(root.trueValue) + val falseOptimized = constantFolding(root.falseValue) + + // ์กฐ๊ฑด์ด ์ƒ์ˆ˜์ธ ๊ฒฝ์šฐ ๋ถ„๊ธฐ ์„ ํƒ + when (conditionOptimized) { + is BooleanNode -> if (conditionOptimized.value) trueOptimized else falseOptimized + is NumberNode -> if (conditionOptimized.value != 0.0) trueOptimized else falseOptimized + else -> factory.createIf(conditionOptimized, trueOptimized, falseOptimized) + } + } + + is ArgumentsNode -> { + val optimizedArgs = root.arguments.map { constantFolding(it) } + factory.createArguments(optimizedArgs) + } + + else -> root + } + } + + /** + * ํ•ญ๋“ฑ ์›์†Œ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun eliminateIdentityElements(root: ASTNode): ASTNode { + return when (root) { + is BinaryOpNode -> { + val leftOptimized = eliminateIdentityElements(root.left) + val rightOptimized = eliminateIdentityElements(root.right) + + when (root.operator) { + "+" -> { + when { + isZero(leftOptimized) -> rightOptimized + isZero(rightOptimized) -> leftOptimized + else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + } + "-" -> { + when { + isZero(rightOptimized) -> leftOptimized + else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + } + "*" -> { + when { + isZero(leftOptimized) || isZero(rightOptimized) -> factory.createNumber(0.0) + isOne(leftOptimized) -> rightOptimized + isOne(rightOptimized) -> leftOptimized + else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + } + "/" -> { + when { + isZero(leftOptimized) -> factory.createNumber(0.0) + isOne(rightOptimized) -> leftOptimized + else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + } + "^" -> { + when { + isOne(rightOptimized) -> leftOptimized // x^1 = x + isOne(leftOptimized) -> factory.createNumber(1.0) // 1^x = 1 + + isZero(leftOptimized) && rightOptimized is NumberNode && rightOptimized.value > 0.0 -> { + factory.createNumber(0.0) + } + + isZero(rightOptimized) && leftOptimized is NumberNode && leftOptimized.value != 0.0 -> { + factory.createNumber(1.0) + } + + else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + } + else -> factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + } + + is UnaryOpNode -> { + val operandOptimized = eliminateIdentityElements(root.operand) + + when (root.operator) { + "+" -> operandOptimized // ๋‹จํ•ญ ํ”Œ๋Ÿฌ์Šค ์ œ๊ฑฐ + "-" -> { + // ์ด์ค‘ ๋ถ€์ • ์ œ๊ฑฐ + if (operandOptimized is UnaryOpNode && operandOptimized.operator == "-") { + operandOptimized.operand + } else { + factory.createUnaryOp(root.operator, operandOptimized) + } + } + else -> factory.createUnaryOp(root.operator, operandOptimized) + } + } + + is FunctionCallNode -> { + val optimizedArgs = root.args.map { eliminateIdentityElements(it) } + factory.createFunctionCall(root.name, optimizedArgs) + } + + is IfNode -> { + val conditionOptimized = eliminateIdentityElements(root.condition) + val trueOptimized = eliminateIdentityElements(root.trueValue) + val falseOptimized = eliminateIdentityElements(root.falseValue) + factory.createIf(conditionOptimized, trueOptimized, falseOptimized) + } + + is ArgumentsNode -> { + val optimizedArgs = root.arguments.map { eliminateIdentityElements(it) } + factory.createArguments(optimizedArgs) + } + + else -> root + } + } + + /** + * ๋ถˆํ•„์š”ํ•œ ์กฐ๊ฑด๋ฌธ์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun eliminateUnnecessaryConditionals(root: ASTNode): ASTNode { + return when (root) { + is IfNode -> { + val conditionOptimized = eliminateUnnecessaryConditionals(root.condition) + val trueOptimized = eliminateUnnecessaryConditionals(root.trueValue) + val falseOptimized = eliminateUnnecessaryConditionals(root.falseValue) + + // ์ฐธ ๊ฐ’๊ณผ ๊ฑฐ์ง“ ๊ฐ’์ด ๊ฐ™์€ ๊ฒฝ์šฐ + if (nodesEqual(trueOptimized, falseOptimized)) { + trueOptimized + } else { + factory.createIf(conditionOptimized, trueOptimized, falseOptimized) + } + } + + is BinaryOpNode -> { + val leftOptimized = eliminateUnnecessaryConditionals(root.left) + val rightOptimized = eliminateUnnecessaryConditionals(root.right) + factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + + is UnaryOpNode -> { + val operandOptimized = eliminateUnnecessaryConditionals(root.operand) + factory.createUnaryOp(root.operator, operandOptimized) + } + + is FunctionCallNode -> { + val optimizedArgs = root.args.map { eliminateUnnecessaryConditionals(it) } + factory.createFunctionCall(root.name, optimizedArgs) + } + + is ArgumentsNode -> { + val optimizedArgs = root.arguments.map { eliminateUnnecessaryConditionals(it) } + factory.createArguments(optimizedArgs) + } + + else -> root + } + } + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ์ž๋ฅผ ๋‹จ์ˆœํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun simplifyUnaryOperations(root: ASTNode): ASTNode { + return when (root) { + is UnaryOpNode -> { + val operandOptimized = simplifyUnaryOperations(root.operand) + + when (root.operator) { + "!" -> { + // ์ด์ค‘ ๋ถ€์ • ์ œ๊ฑฐ + if (operandOptimized is UnaryOpNode && operandOptimized.operator == "!") { + operandOptimized.operand + } else { + factory.createUnaryOp(root.operator, operandOptimized) + } + } + else -> factory.createUnaryOp(root.operator, operandOptimized) + } + } + + is BinaryOpNode -> { + val leftOptimized = simplifyUnaryOperations(root.left) + val rightOptimized = simplifyUnaryOperations(root.right) + factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + + is FunctionCallNode -> { + val optimizedArgs = root.args.map { simplifyUnaryOperations(it) } + factory.createFunctionCall(root.name, optimizedArgs) + } + + is IfNode -> { + val conditionOptimized = simplifyUnaryOperations(root.condition) + val trueOptimized = simplifyUnaryOperations(root.trueValue) + val falseOptimized = simplifyUnaryOperations(root.falseValue) + factory.createIf(conditionOptimized, trueOptimized, falseOptimized) + } + + is ArgumentsNode -> { + val optimizedArgs = root.arguments.map { simplifyUnaryOperations(it) } + factory.createArguments(optimizedArgs) + } + + else -> root + } + } + + /** + * ์ค‘๋ณต ๋…ธ๋“œ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun eliminateDuplicateNodes(root: ASTNode): ASTNode { + val seenNodes = mutableMapOf() + return eliminateDuplicateNodesHelper(root, seenNodes) + } + + /** + * ์ค‘๋ณต ๋…ธ๋“œ ์ œ๊ฑฐ ํ—ฌํผ ํ•จ์ˆ˜ + */ + private fun eliminateDuplicateNodesHelper(root: ASTNode, seenNodes: MutableMap): ASTNode { + val nodeKey = root.toString() + + // ์ด๋ฏธ ๋ณธ ๋…ธ๋“œ์ธ ๊ฒฝ์šฐ ์žฌ์‚ฌ์šฉ + if (seenNodes.containsKey(nodeKey) && root.isLeaf()) { + return seenNodes[nodeKey]!! + } + + val optimized = when (root) { + is BinaryOpNode -> { + val leftOptimized = eliminateDuplicateNodesHelper(root.left, seenNodes) + val rightOptimized = eliminateDuplicateNodesHelper(root.right, seenNodes) + factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + + is UnaryOpNode -> { + val operandOptimized = eliminateDuplicateNodesHelper(root.operand, seenNodes) + factory.createUnaryOp(root.operator, operandOptimized) + } + + is FunctionCallNode -> { + val optimizedArgs = root.args.map { eliminateDuplicateNodesHelper(it, seenNodes) } + factory.createFunctionCall(root.name, optimizedArgs) + } + + is IfNode -> { + val conditionOptimized = eliminateDuplicateNodesHelper(root.condition, seenNodes) + val trueOptimized = eliminateDuplicateNodesHelper(root.trueValue, seenNodes) + val falseOptimized = eliminateDuplicateNodesHelper(root.falseValue, seenNodes) + factory.createIf(conditionOptimized, trueOptimized, falseOptimized) + } + + is ArgumentsNode -> { + val optimizedArgs = root.arguments.map { eliminateDuplicateNodesHelper(it, seenNodes) } + factory.createArguments(optimizedArgs) + } + + else -> root + } + + // ๋ฆฌํ”„ ๋…ธ๋“œ์ธ ๊ฒฝ์šฐ ์ €์žฅ + if (optimized.isLeaf()) { + seenNodes[nodeKey] = optimized + } + + return optimized + } + + /** + * ๊ณตํ†ต ํ•˜์œ„ ํ‘œํ˜„์‹์„ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun eliminateCommonSubexpressions(root: ASTNode): ASTNode { + val subexpressions = mutableMapOf() + return eliminateCommonSubexpressionsHelper(root, subexpressions) + } + + /** + * ๊ณตํ†ต ํ•˜์œ„ ํ‘œํ˜„์‹ ์ œ๊ฑฐ ํ—ฌํผ ํ•จ์ˆ˜ + */ + private fun eliminateCommonSubexpressionsHelper(root: ASTNode, subexpressions: MutableMap): ASTNode { + val nodeKey = root.toString() + + // ์ด๋ฏธ ๋ณธ ํ•˜์œ„ ํ‘œํ˜„์‹์ธ ๊ฒฝ์šฐ ์žฌ์‚ฌ์šฉ + if (subexpressions.containsKey(nodeKey) && !root.isLeaf()) { + return subexpressions[nodeKey]!! + } + + val optimized = when (root) { + is BinaryOpNode -> { + val leftOptimized = eliminateCommonSubexpressionsHelper(root.left, subexpressions) + val rightOptimized = eliminateCommonSubexpressionsHelper(root.right, subexpressions) + factory.createBinaryOp(leftOptimized, root.operator, rightOptimized) + } + + is UnaryOpNode -> { + val operandOptimized = eliminateCommonSubexpressionsHelper(root.operand, subexpressions) + factory.createUnaryOp(root.operator, operandOptimized) + } + + is FunctionCallNode -> { + val optimizedArgs = root.args.map { eliminateCommonSubexpressionsHelper(it, subexpressions) } + factory.createFunctionCall(root.name, optimizedArgs) + } + + is IfNode -> { + val conditionOptimized = eliminateCommonSubexpressionsHelper(root.condition, subexpressions) + val trueOptimized = eliminateCommonSubexpressionsHelper(root.trueValue, subexpressions) + val falseOptimized = eliminateCommonSubexpressionsHelper(root.falseValue, subexpressions) + factory.createIf(conditionOptimized, trueOptimized, falseOptimized) + } + + is ArgumentsNode -> { + val optimizedArgs = root.arguments.map { eliminateCommonSubexpressionsHelper(it, subexpressions) } + factory.createArguments(optimizedArgs) + } + + else -> root + } + + // ๋ณตํ•ฉ ๋…ธ๋“œ์ธ ๊ฒฝ์šฐ ์ €์žฅ + if (!optimized.isLeaf()) { + subexpressions[nodeKey] = optimized + } + + return optimized + } + + /** + * ์ƒ์ˆ˜ ์ดํ•ญ ์—ฐ์‚ฐ์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun evaluateConstantBinaryOp(left: NumberNode, operator: String, right: NumberNode): ASTNode { + return try { + val result = when (operator) { + "+" -> left.value + right.value + "-" -> left.value - right.value + "*" -> left.value * right.value + "/" -> if (right.value != 0.0) left.value / right.value else return factory.createBinaryOp(left, operator, right) + "%" -> if (right.value != 0.0) left.value % right.value else return factory.createBinaryOp(left, operator, right) + "^" -> left.value.pow(right.value) + else -> return factory.createBinaryOp(left, operator, right) + } + factory.createNumber(result) + } catch (e: ArithmeticException) { + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์›๋ณธ ๋…ธ๋“œ ๋ฐ˜ํ™˜ + throw DomainException( + errorCode = ErrorCode.MATH_ERROR, + message = "์ดํ•ญ ์—ฐ์‚ฐ ์ค‘ ์‚ฐ์ˆ  ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "operator" to operator, + "leftValue" to left.value, + "rightValue" to right.value + ) + ) + } catch (e: NumberFormatException) { + // ์ˆซ์ž ํ˜•์‹ ์˜ค๋ฅ˜๋„ ์ฒ˜๋ฆฌ + throw DomainException( + errorCode = ErrorCode.NUMBER_CONVERSION_ERROR, + message = "์ˆซ์ž ๋ณ€ํ™˜ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "operator" to operator, + "leftValue" to left.value, + "rightValue" to right.value + ) + ) + } + } + + /** + * ์ƒ์ˆ˜ ๋‹จํ•ญ ์—ฐ์‚ฐ์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun evaluateConstantUnaryOp(operator: String, operand: NumberNode): ASTNode { + return try { + val result = when (operator) { + "-" -> -operand.value + "+" -> operand.value + else -> return factory.createUnaryOp(operator, operand) + } + factory.createNumber(result) + } catch (e: ArithmeticException) { + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋กœ ๋ณ€ํ™˜ + throw DomainException( + errorCode = ErrorCode.MATH_ERROR, + message = "๋‹จํ•ญ ์—ฐ์‚ฐ ์ค‘ ์‚ฐ์ˆ  ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "operator" to operator, + "operandValue" to operand.value + ) + ) + } catch (e: NumberFormatException) { + // ์ˆซ์ž ํ˜•์‹ ์˜ค๋ฅ˜๋„ ์ฒ˜๋ฆฌ + throw DomainException( + errorCode = ErrorCode.NUMBER_CONVERSION_ERROR, + message = "์ˆซ์ž ๋ณ€ํ™˜ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "operator" to operator, + "operandValue" to operand.value + ) + ) + } + } + + /** + * ์ƒ์ˆ˜ ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun evaluateConstantFunctionCall(name: String, args: List): ASTNode { + return try { + val result = when (name.uppercase()) { + "ABS" -> if (args.size == 1) kotlin.math.abs(args[0].value) else return factory.createFunctionCall(name, args) + "SQRT" -> if (args.size == 1) kotlin.math.sqrt(args[0].value) else return factory.createFunctionCall(name, args) + "SIN" -> if (args.size == 1) kotlin.math.sin(args[0].value) else return factory.createFunctionCall(name, args) + "COS" -> if (args.size == 1) kotlin.math.cos(args[0].value) else return factory.createFunctionCall(name, args) + "TAN" -> if (args.size == 1) kotlin.math.tan(args[0].value) else return factory.createFunctionCall(name, args) + "LOG" -> if (args.size == 1) kotlin.math.ln(args[0].value) else return factory.createFunctionCall(name, args) + "EXP" -> if (args.size == 1) kotlin.math.exp(args[0].value) else return factory.createFunctionCall(name, args) + "POW" -> if (args.size == 2) args[0].value.pow(args[1].value) else return factory.createFunctionCall(name, args) + "MAX" -> if (args.isNotEmpty()) args.maxOf { it.value } else return factory.createFunctionCall(name, args) + "MIN" -> if (args.isNotEmpty()) args.minOf { it.value } else return factory.createFunctionCall(name, args) + else -> return factory.createFunctionCall(name, args) + } + factory.createNumber(result) + } catch (e: ArithmeticException) { + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋กœ ๋ณ€ํ™˜ + throw DomainException( + errorCode = ErrorCode.MATH_ERROR, + message = "ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ค‘ ์‚ฐ์ˆ  ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "functionName" to name, + "argumentCount" to args.size, + "arguments" to args.map { it.value } + ) + ) + } catch (e: NumberFormatException) { + // ์ˆซ์ž ํ˜•์‹ ์˜ค๋ฅ˜๋„ ์ฒ˜๋ฆฌ + throw DomainException( + errorCode = ErrorCode.NUMBER_CONVERSION_ERROR, + message = "ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ค‘ ์ˆซ์ž ๋ณ€ํ™˜ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "functionName" to name, + "argumentCount" to args.size, + "arguments" to args.map { it.value } + ) + ) + } catch (e: IllegalArgumentException) { + // ์ž˜๋ชป๋œ ์ธ์ˆ˜ (์˜ˆ: SQRT(-1), LOG(0) ๋“ฑ) + throw DomainException( + errorCode = ErrorCode.WRONG_ARGUMENT_COUNT, + message = "ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ค‘ ์ž˜๋ชป๋œ ์ธ์ˆ˜: ${e.message}", + cause = e, + context = mapOf( + "functionName" to name, + "argumentCount" to args.size, + "arguments" to args.map { it.value } + ) + ) + } + } + + /** + * ๋…ธ๋“œ๊ฐ€ 0์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isZero(node: ASTNode): Boolean { + return node is NumberNode && node.value == 0.0 + } + + /** + * ๋…ธ๋“œ๊ฐ€ 1์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isOne(node: ASTNode): Boolean { + return node is NumberNode && node.value == 1.0 + } + + /** + * ๋…ธ๋“œ๊ฐ€ ์–‘์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isPositive(node: ASTNode): Boolean { + return node is NumberNode && node.value > 0.0 + } + + /** + * ๋…ธ๋“œ๊ฐ€ ์Œ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isNegative(node: ASTNode): Boolean { + return node is NumberNode && node.value < 0.0 + } + + /** + * ๋‘ ๋…ธ๋“œ๊ฐ€ ๊ตฌ์กฐ์ ์œผ๋กœ ๊ฐ™์€์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun nodesEqual(node1: ASTNode, node2: ASTNode): Boolean { + return node1.isStructurallyEqual(node2) + } + + /** + * ์ตœ์ ํ™” ํ†ต๊ณ„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun calculateOptimizationStatistics(original: ASTNode, optimized: ASTNode): OptimizationStatistics { + val originalStats = traverser.calculateStatistics(original) + val optimizedStats = traverser.calculateStatistics(optimized) + + return OptimizationStatistics( + originalNodeCount = originalStats.nodeCount, + optimizedNodeCount = optimizedStats.nodeCount, + reductionRatio = calculateReductionRatio(originalStats.nodeCount, optimizedStats.nodeCount), + originalDepth = originalStats.maxDepth, + optimizedDepth = optimizedStats.maxDepth, + depthReduction = TreeDepth.of(originalStats.maxDepth.value - optimizedStats.maxDepth.value) + ) + } + + /** + * ๊ฐ์†Œ ๋น„์œจ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun calculateReductionRatio(original: NodeSize, optimized: NodeSize): Double { + return if (original.value > 0) { + (original.value - optimized.value).toDouble() / original.value.toDouble() + } else { + 0.0 + } + } + + /** + * ์ตœ์ ํ™” ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + */ + data class OptimizationStatistics( + val originalNodeCount: NodeSize, + val optimizedNodeCount: NodeSize, + val reductionRatio: Double, + val originalDepth: TreeDepth, + val optimizedDepth: TreeDepth, + val depthReduction: TreeDepth + ) { + /** + * ๋…ธ๋“œ ์ˆ˜ ๊ฐ์†Œ๋Ÿ‰์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getNodeReduction(): NodeSize { + return NodeSize.of(originalNodeCount.value - optimizedNodeCount.value) + } + + /** + * ์ตœ์ ํ™” ํšจ๊ณผ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun hasOptimizationEffect(): Boolean { + return reductionRatio > 0.0 || depthReduction.value > 0 + } + } + + companion object { + private const val MAX_OPTIMIZATION_PASSES = 10 + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeTraverser.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeTraverser.kt new file mode 100644 index 00000000..847a071c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/services/TreeTraverser.kt @@ -0,0 +1,422 @@ +package hs.kr.entrydsm.domain.ast.services + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.domain.ast.values.NodeSize +import hs.kr.entrydsm.domain.ast.values.NodeType +import hs.kr.entrydsm.domain.ast.values.TreeDepth +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType + +/** + * AST ํŠธ๋ฆฌ๋ฅผ ์ˆœํšŒํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋‹ค์–‘ํ•œ ์ˆœํšŒ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•˜๋ฉฐ, ์ˆœํšŒ ์ค‘ ํŠน์ • ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” + * ๋…ธ๋“œ๋ฅผ ์ฐพ๊ฑฐ๋‚˜ ๋ณ€ํ˜•ํ•˜๋Š” ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Service( + name = "AST ํŠธ๋ฆฌ ์ˆœํšŒ ์„œ๋น„์Šค", + type = ServiceType.DOMAIN_SERVICE +) +class TreeTraverser { + + /** + * ์ „์œ„ ์ˆœํšŒ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param root ์ˆœํšŒํ•  ๋ฃจํŠธ ๋…ธ๋“œ + * @param visitor ๋ฐฉ๋ฌธ์ž + */ + fun preOrderTraversal(root: ASTNode, visitor: ASTVisitor) { + visitNode(root, visitor) + root.getChildren().forEach { child -> + preOrderTraversal(child, visitor) + } + } + + /** + * ์ค‘์œ„ ์ˆœํšŒ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค (์ด์ง„ ํŠธ๋ฆฌ์— ์ ํ•ฉ). + * + * @param root ์ˆœํšŒํ•  ๋ฃจํŠธ ๋…ธ๋“œ + * @param visitor ๋ฐฉ๋ฌธ์ž + */ + fun inOrderTraversal(root: ASTNode, visitor: ASTVisitor) { + val children = root.getChildren() + + if (children.isNotEmpty()) { + inOrderTraversal(children[0], visitor) + } + + visitNode(root, visitor) + + if (children.size > 1) { + for (i in 1 until children.size) { + inOrderTraversal(children[i], visitor) + } + } + } + + /** + * ํ›„์œ„ ์ˆœํšŒ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param root ์ˆœํšŒํ•  ๋ฃจํŠธ ๋…ธ๋“œ + * @param visitor ๋ฐฉ๋ฌธ์ž + */ + fun postOrderTraversal(root: ASTNode, visitor: ASTVisitor) { + root.getChildren().forEach { child -> + postOrderTraversal(child, visitor) + } + visitNode(root, visitor) + } + + /** + * ๋ ˆ๋ฒจ ์ˆœํšŒ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param root ์ˆœํšŒํ•  ๋ฃจํŠธ ๋…ธ๋“œ + * @param visitor ๋ฐฉ๋ฌธ์ž + */ + fun levelOrderTraversal(root: ASTNode, visitor: ASTVisitor) { + val queue = mutableListOf() + queue.add(root) + + while (queue.isNotEmpty()) { + val current = queue.removeFirst() + visitNode(current, visitor) + queue.addAll(current.getChildren()) + } + } + + /** + * ๊นŠ์ด ์šฐ์„  ํƒ์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param root ํƒ์ƒ‰ํ•  ๋ฃจํŠธ ๋…ธ๋“œ + * @param condition ์กฐ๊ฑด ํ•จ์ˆ˜ + * @return ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ ๋…ธ๋“œ + */ + fun depthFirstSearch(root: ASTNode, condition: (ASTNode) -> Boolean): ASTNode? { + if (condition(root)) { + return root + } + + for (child in root.getChildren()) { + val result = depthFirstSearch(child, condition) + if (result != null) { + return result + } + } + + return null + } + + /** + * ๋„ˆ๋น„ ์šฐ์„  ํƒ์ƒ‰์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param root ํƒ์ƒ‰ํ•  ๋ฃจํŠธ ๋…ธ๋“œ + * @param condition ์กฐ๊ฑด ํ•จ์ˆ˜ + * @return ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ ๋…ธ๋“œ + */ + fun breadthFirstSearch(root: ASTNode, condition: (ASTNode) -> Boolean): ASTNode? { + val queue = mutableListOf() + queue.add(root) + + while (queue.isNotEmpty()) { + val current = queue.removeFirst() + if (condition(current)) { + return current + } + queue.addAll(current.getChildren()) + } + + return null + } + + /** + * ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ๋ชจ๋“  ๋…ธ๋“œ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param root ํƒ์ƒ‰ํ•  ๋ฃจํŠธ ๋…ธ๋“œ + * @param condition ์กฐ๊ฑด ํ•จ์ˆ˜ + * @return ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ๋ชจ๋“  ๋…ธ๋“œ + */ + fun findAll(root: ASTNode, condition: (ASTNode) -> Boolean): List { + val result = mutableListOf() + + if (condition(root)) { + result.add(root) + } + + for (child in root.getChildren()) { + result.addAll(findAll(child, condition)) + } + + return result + } + + /** + * ํŠน์ • ํƒ€์ž…์˜ ๋ชจ๋“  ๋…ธ๋“œ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param root ํƒ์ƒ‰ํ•  ๋ฃจํŠธ ๋…ธ๋“œ + * @param nodeClass ์ฐพ์„ ๋…ธ๋“œ ํด๋ž˜์Šค + * @return ํ•ด๋‹น ํƒ€์ž…์˜ ๋ชจ๋“  ๋…ธ๋“œ + */ + fun findByType(root: ASTNode, nodeClass: Class): List { + val result = mutableListOf() + + if (nodeClass.isInstance(root)) { + @Suppress("UNCHECKED_CAST") + result.add(root as T) + } + + for (child in root.getChildren()) { + result.addAll(findByType(child, nodeClass)) + } + + return result + } + + /** + * ๋…ธ๋“œ์˜ ๊ฒฝ๋กœ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param root ๋ฃจํŠธ ๋…ธ๋“œ + * @param target ์ฐพ์„ ๋…ธ๋“œ + * @return ๋ฃจํŠธ์—์„œ ํƒ€๊ฒŸ๊นŒ์ง€์˜ ๊ฒฝ๋กœ + */ + fun findPath(root: ASTNode, target: ASTNode): List? { + val path = mutableListOf() + + if (findPathHelper(root, target, path)) { + return path + } + + return null + } + + /** + * ๊ฒฝ๋กœ ์ฐพ๊ธฐ ํ—ฌํผ ํ•จ์ˆ˜ + */ + private fun findPathHelper(current: ASTNode, target: ASTNode, path: MutableList): Boolean { + path.add(current) + + if (current == target) { + return true + } + + for (child in current.getChildren()) { + if (findPathHelper(child, target, path)) { + return true + } + } + + path.removeAt(path.size - 1) + return false + } + + /** + * ํŠน์ • ๊นŠ์ด์˜ ๋ชจ๋“  ๋…ธ๋“œ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param root ๋ฃจํŠธ ๋…ธ๋“œ + * @param depth ์ฐพ์„ ๊นŠ์ด + * @return ํ•ด๋‹น ๊นŠ์ด์˜ ๋ชจ๋“  ๋…ธ๋“œ + */ + fun findAtDepth(root: ASTNode, depth: Int): List { + val result = mutableListOf() + findAtDepthHelper(root, depth, 0, result) + return result + } + + /** + * ๊นŠ์ด๋ณ„ ๋…ธ๋“œ ์ฐพ๊ธฐ ํ—ฌํผ ํ•จ์ˆ˜ + */ + private fun findAtDepthHelper(current: ASTNode, targetDepth: Int, currentDepth: Int, result: MutableList) { + if (currentDepth == targetDepth) { + result.add(current) + return + } + + for (child in current.getChildren()) { + findAtDepthHelper(child, targetDepth, currentDepth + 1, result) + } + } + + /** + * ๋ฆฌํ”„ ๋…ธ๋“œ๋“ค์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param root ๋ฃจํŠธ ๋…ธ๋“œ + * @return ๋ชจ๋“  ๋ฆฌํ”„ ๋…ธ๋“œ + */ + fun findLeaves(root: ASTNode): List { + return findAll(root) { it.isLeaf() } + } + + /** + * ๊ฐ€์žฅ ๊นŠ์€ ๋…ธ๋“œ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param root ๋ฃจํŠธ ๋…ธ๋“œ + * @return ๊ฐ€์žฅ ๊นŠ์€ ๋…ธ๋“œ์™€ ๊ทธ ๊นŠ์ด + */ + fun findDeepestNode(root: ASTNode): Pair { + var deepestNode = root + var maxDepth = TreeDepth.zero() + + findDeepestNodeHelper(root, TreeDepth.zero(), { node, depth -> + if (depth.isGreaterThan(maxDepth)) { + deepestNode = node + maxDepth = depth + } + }) + + return Pair(deepestNode, maxDepth) + } + + /** + * ๊ฐ€์žฅ ๊นŠ์€ ๋…ธ๋“œ ์ฐพ๊ธฐ ํ—ฌํผ ํ•จ์ˆ˜ + */ + private fun findDeepestNodeHelper(current: ASTNode, currentDepth: TreeDepth, callback: (ASTNode, TreeDepth) -> Unit) { + callback(current, currentDepth) + + for (child in current.getChildren()) { + findDeepestNodeHelper(child, currentDepth.increment(), callback) + } + } + + /** + * ํŠธ๋ฆฌ์˜ ํ†ต๊ณ„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param root ๋ฃจํŠธ ๋…ธ๋“œ + * @return ํŠธ๋ฆฌ ํ†ต๊ณ„ + */ + fun calculateStatistics(root: ASTNode): TreeStatistics { + var nodeCount = 0 + var leafCount = 0 + var maxDepth = TreeDepth.zero() + var totalDepth = 0 + val nodeTypeCounts = mutableMapOf() + + // ๊นŠ์ด๋ฅผ ์ถ”์ ํ•˜๋ฉด์„œ ์ˆœํšŒํ•˜๋Š” ํ—ฌํผ ํ•จ์ˆ˜ + fun traverseWithDepth(node: ASTNode, currentDepth: Int) { + nodeCount++ + totalDepth += currentDepth + + // ์ตœ๋Œ€ ๊นŠ์ด ์—…๋ฐ์ดํŠธ + if (currentDepth > maxDepth.value) { + maxDepth = TreeDepth.of(currentDepth) + } + + // ๋…ธ๋“œ ํƒ€์ž…๋ณ„ ์ฒ˜๋ฆฌ + when (node) { + is hs.kr.entrydsm.domain.ast.entities.NumberNode -> { + leafCount++ + updateNodeTypeCount(NodeType.NUMBER, nodeTypeCounts) + } + is hs.kr.entrydsm.domain.ast.entities.BooleanNode -> { + leafCount++ + updateNodeTypeCount(NodeType.BOOLEAN, nodeTypeCounts) + } + is hs.kr.entrydsm.domain.ast.entities.VariableNode -> { + leafCount++ + updateNodeTypeCount(NodeType.VARIABLE, nodeTypeCounts) + } + is hs.kr.entrydsm.domain.ast.entities.BinaryOpNode -> { + updateNodeTypeCount(NodeType.BINARY_OP, nodeTypeCounts) + // ์ž์‹ ๋…ธ๋“œ๋“ค์„ ๋” ๊นŠ์€ ๋ ˆ๋ฒจ์—์„œ ์ˆœํšŒ + traverseWithDepth(node.left, currentDepth + 1) + traverseWithDepth(node.right, currentDepth + 1) + } + is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode -> { + updateNodeTypeCount(NodeType.UNARY_OP, nodeTypeCounts) + // ์ž์‹ ๋…ธ๋“œ๋ฅผ ๋” ๊นŠ์€ ๋ ˆ๋ฒจ์—์„œ ์ˆœํšŒ + traverseWithDepth(node.operand, currentDepth + 1) + } + is hs.kr.entrydsm.domain.ast.entities.FunctionCallNode -> { + updateNodeTypeCount(NodeType.FUNCTION_CALL, nodeTypeCounts) + // ๋ชจ๋“  ์ธ์ˆ˜๋“ค์„ ๋” ๊นŠ์€ ๋ ˆ๋ฒจ์—์„œ ์ˆœํšŒ + node.args.forEach { arg -> + traverseWithDepth(arg, currentDepth + 1) + } + } + is hs.kr.entrydsm.domain.ast.entities.IfNode -> { + updateNodeTypeCount(NodeType.IF, nodeTypeCounts) + // ์กฐ๊ฑด, ์ฐธ ๊ฐ’, ๊ฑฐ์ง“ ๊ฐ’์„ ๋” ๊นŠ์€ ๋ ˆ๋ฒจ์—์„œ ์ˆœํšŒ + traverseWithDepth(node.condition, currentDepth + 1) + traverseWithDepth(node.trueValue, currentDepth + 1) + traverseWithDepth(node.falseValue, currentDepth + 1) + } + is hs.kr.entrydsm.domain.ast.entities.ArgumentsNode -> { + updateNodeTypeCount(NodeType.ARGUMENTS, nodeTypeCounts) + // ๋ชจ๋“  ์ธ์ˆ˜๋“ค์„ ๋” ๊นŠ์€ ๋ ˆ๋ฒจ์—์„œ ์ˆœํšŒ + node.arguments.forEach { arg -> + traverseWithDepth(arg, currentDepth + 1) + } + } + } + } + + // ๋ฃจํŠธ ๋…ธ๋“œ๋ถ€ํ„ฐ ๊นŠ์ด 0์—์„œ ์‹œ์ž‘ + traverseWithDepth(root, 0) + + return TreeStatistics( + nodeCount = NodeSize.of(nodeCount), + leafCount = NodeSize.of(leafCount), + maxDepth = maxDepth, + averageDepth = if (nodeCount > 0) TreeDepth.of(totalDepth / nodeCount) else TreeDepth.zero(), + nodeTypeCounts = nodeTypeCounts.toMap() + ) + } + + /** + * ๋…ธ๋“œ ํƒ€์ž… ์นด์šดํŠธ ์—…๋ฐ์ดํŠธ + */ + private fun updateNodeTypeCount(type: NodeType, counts: MutableMap) { + counts[type] = counts.getOrDefault(type, 0) + 1 + } + + /** + * ๋…ธ๋“œ ๋ฐฉ๋ฌธ ์ฒ˜๋ฆฌ + */ + private fun visitNode(node: ASTNode, visitor: ASTVisitor) { + node.accept(visitor) + } + + /** + * ํŠธ๋ฆฌ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + */ + data class TreeStatistics( + val nodeCount: NodeSize, + val leafCount: NodeSize, + val maxDepth: TreeDepth, + val averageDepth: TreeDepth, + val nodeTypeCounts: Map + ) { + /** + * ๋ฆฌํ”„ ๋…ธ๋“œ ๋น„์œจ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun getLeafRatio(): Double { + return if (nodeCount.value > 0) { + leafCount.value.toDouble() / nodeCount.value.toDouble() + } else { + 0.0 + } + } + + /** + * ๊ฐ€์žฅ ๋งŽ์€ ๋…ธ๋“œ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getMostCommonNodeType(): NodeType? { + return nodeTypeCounts.maxByOrNull { it.value }?.key + } + + /** + * ํŠธ๋ฆฌ ๋ฐ€๋„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun getTreeDensity(): Double { + return if (maxDepth.value > 0) { + nodeCount.value.toDouble() / maxDepth.value.toDouble() + } else { + 0.0 + } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/ASTValiditySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/ASTValiditySpec.kt new file mode 100644 index 00000000..15cfa645 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/ASTValiditySpec.kt @@ -0,0 +1,415 @@ +package hs.kr.entrydsm.domain.ast.specifications + +import hs.kr.entrydsm.domain.ast.entities.* +import hs.kr.entrydsm.domain.ast.utils.FunctionValidationRules +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.SpecificationResult +import hs.kr.entrydsm.global.annotation.specification.SpecificationContract +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * AST ๋…ธ๋“œ ์œ ํšจ์„ฑ ์‚ฌ์–‘์„ ์ •์˜ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * AST ๋…ธ๋“œ๊ฐ€ ๋„๋ฉ”์ธ ๊ทœ์น™์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋ฉฐ, + * ๋ณตํ•ฉ ์‚ฌ์–‘์„ ํ†ตํ•ด ๋ณต์žกํ•œ ๊ฒ€์ฆ ๋กœ์ง์„ ๊ตฌ์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Specification( + name = "AST ๋…ธ๋“œ ์œ ํšจ์„ฑ ์‚ฌ์–‘", + description = "AST ๋…ธ๋“œ๊ฐ€ ๋„๋ฉ”์ธ ๊ทœ์น™์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ์‚ฌ์–‘", + domain = "ast", + priority = Priority.HIGH +) +class ASTValiditySpec : SpecificationContract { + + override fun isSatisfiedBy(node: ASTNode): Boolean { + return when (node) { + is NumberNode -> isValidNumberNode(node) + is BooleanNode -> isValidBooleanNode(node) + is VariableNode -> isValidVariableNode(node) + is BinaryOpNode -> isValidBinaryOpNode(node) + is UnaryOpNode -> isValidUnaryOpNode(node) + is FunctionCallNode -> isValidFunctionCallNode(node) + is IfNode -> isValidIfNode(node) + is ArgumentsNode -> isValidArgumentsNode(node) + else -> false + } + } + + fun getWhyNotSatisfied(node: ASTNode): String { + return when (node) { + is NumberNode -> getNumberNodeViolations(node) + is BooleanNode -> getBooleanNodeViolations(node) + is VariableNode -> getVariableNodeViolations(node) + is BinaryOpNode -> getBinaryOpNodeViolations(node) + is UnaryOpNode -> getUnaryOpNodeViolations(node) + is FunctionCallNode -> getFunctionCallNodeViolations(node) + is IfNode -> getIfNodeViolations(node) + is ArgumentsNode -> getArgumentsNodeViolations(node) + else -> Msg.unsupportedNodeType(node::class.simpleName ?: UNKNOWN) + } + } + + fun getValidationResult(node: ASTNode): SpecificationResult { + val isValid = isSatisfiedBy(node) + val message = if (isValid) Msg.VALIDATION_SUCCESS else getWhyNotSatisfied(node) + return SpecificationResult( + success = isValid, + message = message, + specification = this + ) + } + + // ---- validators ---- + + private fun isValidNumberNode(node: NumberNode): Boolean { + return node.value.isFinite() && + !node.value.isNaN() && + node.value >= MIN_NUMBER_VALUE && + node.value <= MAX_NUMBER_VALUE + } + + private fun isValidBooleanNode(@Suppress("UNUSED_PARAMETER") node: BooleanNode): Boolean = true + + private fun isValidVariableNode(node: VariableNode): Boolean { + return node.name.isNotBlank() && + node.name.length <= MAX_VARIABLE_NAME_LENGTH && + isValidVariableName(node.name) && + !isReservedWord(node.name) + } + + private fun isValidBinaryOpNode(node: BinaryOpNode): Boolean { + return node.operator.isNotBlank() && + isSupportedBinaryOperator(node.operator) && + isSatisfiedBy(node.left) && + isSatisfiedBy(node.right) && + isValidBinaryOperation(node.left, node.operator, node.right) + } + + private fun isValidUnaryOpNode(node: UnaryOpNode): Boolean { + return node.operator.isNotBlank() && + isSupportedUnaryOperator(node.operator) && + isSatisfiedBy(node.operand) && + isValidUnaryOperation(node.operator, node.operand) + } + + private fun isValidFunctionCallNode(node: FunctionCallNode): Boolean { + return node.name.isNotBlank() && + node.name.length <= MAX_FUNCTION_NAME_LENGTH && + isValidFunctionName(node.name) && + node.args.size <= MAX_FUNCTION_ARGS && + node.args.all { isSatisfiedBy(it) } && + isValidFunctionCall(node.name, node.args) + } + + private fun isValidIfNode(node: IfNode): Boolean { + return isSatisfiedBy(node.condition) && + isSatisfiedBy(node.trueValue) && + isSatisfiedBy(node.falseValue) && + node.getDepth() <= MAX_NODE_DEPTH && + node.getSize() <= MAX_NODE_SIZE + } + + private fun isValidArgumentsNode(node: ArgumentsNode): Boolean { + return node.arguments.size <= MAX_ARGUMENTS_COUNT && + node.arguments.all { isSatisfiedBy(it) } + } + + // ---- violation builders ---- + + private fun getNumberNodeViolations(node: NumberNode): String { + val violations = mutableListOf() + if (!node.value.isFinite()) { + violations.add(Msg.numberNotFinite(node.value)) + } + if (node.value.isNaN()) { + violations.add(Msg.numberIsNaN()) + } + if (node.value < MIN_NUMBER_VALUE) { + violations.add(Msg.numberBelowMin(node.value, MIN_NUMBER_VALUE)) + } + if (node.value > MAX_NUMBER_VALUE) { + violations.add(Msg.numberAboveMax(node.value, MAX_NUMBER_VALUE)) + } + return violations.joinToString("; ") + } + + private fun getBooleanNodeViolations(@Suppress("UNUSED_PARAMETER") node: BooleanNode): String { + return Msg.BOOLEAN_ALWAYS_VALID + } + + private fun getVariableNodeViolations(node: VariableNode): String { + val violations = mutableListOf() + if (node.name.isBlank()) { + violations.add(Msg.variableNameBlank()) + } + if (node.name.length > MAX_VARIABLE_NAME_LENGTH) { + violations.add(Msg.variableNameTooLong(node.name.length, MAX_VARIABLE_NAME_LENGTH)) + } + if (!isValidVariableName(node.name)) { + violations.add(Msg.variableNameInvalid(node.name)) + } + if (isReservedWord(node.name)) { + violations.add(Msg.variableNameReserved(node.name)) + } + return violations.joinToString("; ") + } + + private fun getBinaryOpNodeViolations(node: BinaryOpNode): String { + val violations = mutableListOf() + if (node.operator.isBlank()) { + violations.add(Msg.operatorBlank()) + } + if (!isSupportedBinaryOperator(node.operator)) { + violations.add(Msg.binaryOperatorUnsupported(node.operator)) + } + if (!isSatisfiedBy(node.left)) { + violations.add(Msg.leftOperandInvalid(getWhyNotSatisfied(node.left))) + } + if (!isSatisfiedBy(node.right)) { + violations.add(Msg.rightOperandInvalid(getWhyNotSatisfied(node.right))) + } + if (!isValidBinaryOperation(node.left, node.operator, node.right)) { + violations.add(Msg.binaryOperationInvalid(node.left, node.operator, node.right)) + } + return violations.joinToString("; ") + } + + private fun getUnaryOpNodeViolations(node: UnaryOpNode): String { + val violations = mutableListOf() + if (node.operator.isBlank()) { + violations.add(Msg.operatorBlank()) + } + if (!isSupportedUnaryOperator(node.operator)) { + violations.add(Msg.unaryOperatorUnsupported(node.operator)) + } + if (!isSatisfiedBy(node.operand)) { + violations.add(Msg.operandInvalid(getWhyNotSatisfied(node.operand))) + } + if (!isValidUnaryOperation(node.operator, node.operand)) { + violations.add(Msg.unaryOperationInvalid(node.operator, node.operand)) + } + return violations.joinToString("; ") + } + + private fun getFunctionCallNodeViolations(node: FunctionCallNode): String { + val violations = mutableListOf() + if (node.name.isBlank()) { + violations.add(Msg.functionNameBlank()) + } + if (node.name.length > MAX_FUNCTION_NAME_LENGTH) { + violations.add(Msg.functionNameTooLong(node.name.length, MAX_FUNCTION_NAME_LENGTH)) + } + if (!isValidFunctionName(node.name)) { + violations.add(Msg.functionNameInvalid(node.name)) + } + if (node.args.size > MAX_FUNCTION_ARGS) { + violations.add(Msg.functionArgsTooMany(node.args.size, MAX_FUNCTION_ARGS)) + } + node.args.forEachIndexed { index, arg -> + if (!isSatisfiedBy(arg)) { + violations.add(Msg.functionArgInvalid(index, getWhyNotSatisfied(arg))) + } + } + if (!isValidFunctionCall(node.name, node.args)) { + violations.add(Msg.functionCallInvalid(node.name, node.args)) + } + return violations.joinToString("; ") + } + + private fun getIfNodeViolations(node: IfNode): String { + val violations = mutableListOf() + if (!isSatisfiedBy(node.condition)) { + violations.add(Msg.ifConditionInvalid(getWhyNotSatisfied(node.condition))) + } + if (!isSatisfiedBy(node.trueValue)) { + violations.add(Msg.ifTrueInvalid(getWhyNotSatisfied(node.trueValue))) + } + if (!isSatisfiedBy(node.falseValue)) { + violations.add(Msg.ifFalseInvalid(getWhyNotSatisfied(node.falseValue))) + } + val depth = node.getDepth() + if (depth > MAX_NODE_DEPTH) { + violations.add(Msg.nodeDepthExceeded(depth, MAX_NODE_DEPTH)) + } + val size = node.getSize() + if (size > MAX_NODE_SIZE) { + violations.add(Msg.nodeSizeExceeded(size, MAX_NODE_SIZE)) + } + return violations.joinToString("; ") + } + + private fun getArgumentsNodeViolations(node: ArgumentsNode): String { + val violations = mutableListOf() + if (node.arguments.size > MAX_ARGUMENTS_COUNT) { + violations.add(Msg.argumentsTooMany(node.arguments.size, MAX_ARGUMENTS_COUNT)) + } + node.arguments.forEachIndexed { index, arg -> + if (!isSatisfiedBy(arg)) { + violations.add(Msg.argumentInvalid(index, getWhyNotSatisfied(arg))) + } + } + return violations.joinToString("; ") + } + + // ---- helpers ---- + + private fun isValidVariableName(name: String): Boolean { + if (name.isEmpty()) return false + if (!name[0].isLetter() && name[0] != '_') return false + return name.drop(1).all { it.isLetterOrDigit() || it == '_' } + } + + private fun isValidFunctionName(name: String): Boolean { + if (name.isEmpty()) return false + if (!name[0].isLetter()) return false + return name.drop(1).all { it.isLetterOrDigit() || it == '_' } + } + + private fun isReservedWord(name: String): Boolean = RESERVED_WORDS.contains(name.lowercase()) + + private fun isSupportedBinaryOperator(operator: String): Boolean = BINARY_OPERATORS.contains(operator) + + private fun isSupportedUnaryOperator(operator: String): Boolean = UNARY_OPERATORS.contains(operator) + + private fun isValidBinaryOperation(left: ASTNode, operator: String, right: ASTNode): Boolean { + return when (operator) { + "/" -> !isZeroConstant(right) + "%" -> !isZeroConstant(right) + "^" -> !(isZeroConstant(left) && isZeroConstant(right)) + else -> true + } + } + + private fun isValidUnaryOperation(operator: String, operand: ASTNode): Boolean { + return when (operator) { + "!" -> true + "-", "+" -> true + else -> false + } + } + + private fun isValidFunctionCall(name: String, args: List): Boolean { + return FunctionValidationRules.isValidFunctionCall(name, args) + } + + private fun isZeroConstant(node: ASTNode): Boolean { + return node is NumberNode && node.value == 0.0 + } + + companion object { + // --- constraints --- + private const val MAX_NUMBER_VALUE = 1e15 + private const val MIN_NUMBER_VALUE = -1e15 + private const val MAX_VARIABLE_NAME_LENGTH = 50 + private const val MAX_FUNCTION_NAME_LENGTH = 50 + private const val MAX_FUNCTION_ARGS = 10 + private const val MAX_ARGUMENTS_COUNT = 100 + private const val MAX_NODE_SIZE = 1000 + private const val MAX_NODE_DEPTH = 50 + + private val RESERVED_WORDS = setOf( + "if", "else", "while", "for", "do", "break", "continue", + "function", "return", "var", "let", "const", "true", "false", + "null", "undefined", "this", "new", "typeof", "instanceof", + "try", "catch", "finally", "throw", "switch", "case", "default" + ) + + private val BINARY_OPERATORS = setOf( + "+", "-", "*", "/", "%", "^", + "==", "!=", "<", "<=", ">", ">=", + "&&", "||" + ) + + private val UNARY_OPERATORS = setOf("-", "+", "!") + + private const val UNKNOWN = "Unknown" + + // --- messages / builders --- + object Msg { + // common + const val VALIDATION_SUCCESS = "๊ฒ€์ฆ ์„ฑ๊ณต" + fun unsupportedNodeType(type: String) = "์ง€์›๋˜์ง€ ์•Š๋Š” ๋…ธ๋“œ ํƒ€์ž…์ž…๋‹ˆ๋‹ค: $type" + + // number + fun numberNotFinite(value: Double) = "์ˆซ์ž ๊ฐ’์ด ์œ ํ•œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $value" + fun numberIsNaN() = "์ˆซ์ž ๊ฐ’์ด NaN์ž…๋‹ˆ๋‹ค" + fun numberBelowMin(value: Double, min: Double) = + "์ˆซ์ž ๊ฐ’์ด ์ตœ์†Œ๊ฐ’ ๋ฏธ๋งŒ์ž…๋‹ˆ๋‹ค: $value < $min" + fun numberAboveMax(value: Double, max: Double) = + "์ˆซ์ž ๊ฐ’์ด ์ตœ๋Œ€๊ฐ’ ์ดˆ๊ณผ์ž…๋‹ˆ๋‹ค: $value > $max" + + // boolean + const val BOOLEAN_ALWAYS_VALID = "" + + // variable + fun variableNameBlank() = "๋ณ€์ˆ˜๋ช…์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค" + fun variableNameTooLong(actual: Int, max: Int) = + "๋ณ€์ˆ˜๋ช…์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค: $actual > $max" + fun variableNameInvalid(name: String) = + "์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช… ํ˜•์‹์ž…๋‹ˆ๋‹ค: $name" + fun variableNameReserved(name: String) = + "์˜ˆ์•ฝ์–ด๋Š” ๋ณ€์ˆ˜๋ช…์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $name" + + // operators (common) + fun operatorBlank() = "์—ฐ์‚ฐ์ž๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค" + + // binary + fun binaryOperatorUnsupported(op: String) = + "์ง€์›๋˜์ง€ ์•Š๋Š” ์ดํ•ญ ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค: $op" + fun leftOperandInvalid(reason: String) = + "์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $reason" + fun rightOperandInvalid(reason: String) = + "์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $reason" + fun binaryOperationInvalid(left: ASTNode, op: String, right: ASTNode) = + "์œ ํšจํ•˜์ง€ ์•Š์€ ์ดํ•ญ ์—ฐ์‚ฐ์ž…๋‹ˆ๋‹ค: $left $op $right" + + // unary + fun unaryOperatorUnsupported(op: String) = + "์ง€์›๋˜์ง€ ์•Š๋Š” ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค: $op" + fun operandInvalid(reason: String) = + "ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $reason" + fun unaryOperationInvalid(op: String, operand: ASTNode) = + "์œ ํšจํ•˜์ง€ ์•Š์€ ๋‹จํ•ญ ์—ฐ์‚ฐ์ž…๋‹ˆ๋‹ค: $op$operand" + + // function call + fun functionNameBlank() = "ํ•จ์ˆ˜๋ช…์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค" + fun functionNameTooLong(actual: Int, max: Int) = + "ํ•จ์ˆ˜๋ช…์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค: $actual > $max" + fun functionNameInvalid(name: String) = + "์œ ํšจํ•˜์ง€ ์•Š์€ ํ•จ์ˆ˜๋ช… ํ˜•์‹์ž…๋‹ˆ๋‹ค: $name" + fun functionArgsTooMany(actual: Int, max: Int) = + "ํ•จ์ˆ˜ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค: $actual > $max" + fun functionArgInvalid(index: Int, reason: String) = + "์ธ์ˆ˜ $index ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $reason" + fun functionCallInvalid(name: String, args: List) = + "์œ ํšจํ•˜์ง€ ์•Š์€ ํ•จ์ˆ˜ ํ˜ธ์ถœ์ž…๋‹ˆ๋‹ค: $name(${args.joinToString(", ")})" + + // if + fun ifConditionInvalid(reason: String) = + "์กฐ๊ฑด์‹์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $reason" + fun ifTrueInvalid(reason: String) = + "์ฐธ ๊ฐ’์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $reason" + fun ifFalseInvalid(reason: String) = + "๊ฑฐ์ง“ ๊ฐ’์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $reason" + fun nodeDepthExceeded(actual: Int, max: Int) = + "๋…ธ๋“œ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค: $actual > $max" + fun nodeSizeExceeded(actual: Int, max: Int) = + "๋…ธ๋“œ ํฌ๊ธฐ๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค: $actual > $max" + + // arguments + fun argumentsTooMany(actual: Int, max: Int) = + "์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค: $actual > $max" + fun argumentInvalid(index: Int, reason: String) = + "์ธ์ˆ˜ $index ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $reason" + } + } + + // SpecificationContract ๊ตฌํ˜„ + override fun getName(): String = "AST ๋…ธ๋“œ ์œ ํšจ์„ฑ ์‚ฌ์–‘" + override fun getDescription(): String = "AST ๋…ธ๋“œ๊ฐ€ ๋„๋ฉ”์ธ ๊ทœ์น™์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•˜๋Š” ์‚ฌ์–‘" + override fun getDomain(): String = "ast" + override fun getPriority(): Priority = Priority.HIGH +} diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/NodeStructureSpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/NodeStructureSpec.kt new file mode 100644 index 00000000..cdb25a67 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/specifications/NodeStructureSpec.kt @@ -0,0 +1,503 @@ +package hs.kr.entrydsm.domain.ast.specifications + +import hs.kr.entrydsm.domain.ast.entities.* +import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.entities.IfNode +import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode +import hs.kr.entrydsm.domain.ast.entities.VariableNode +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.SpecificationResult +import hs.kr.entrydsm.global.annotation.specification.SpecificationContract +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * AST ๋…ธ๋“œ ๊ตฌ์กฐ ์‚ฌ์–‘์„ ์ •์˜ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * AST ๋…ธ๋“œ์˜ ๊ตฌ์กฐ์  ์ •ํ•ฉ์„ฑ๊ณผ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•˜๋ฉฐ, + * ๋…ธ๋“œ ๊ฐ„์˜ ๊ด€๊ณ„์™€ ํŠธ๋ฆฌ ๊ตฌ์กฐ์˜ ์œ ํšจ์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Specification( + name = "AST ๋…ธ๋“œ ๊ตฌ์กฐ ์‚ฌ์–‘", + description = "AST ๋…ธ๋“œ์˜ ๊ตฌ์กฐ์  ์ •ํ•ฉ์„ฑ๊ณผ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ์‚ฌ์–‘", + domain = "ast", + priority = Priority.NORMAL +) +class NodeStructureSpec : SpecificationContract { + + /** + * AST ๋…ธ๋“œ๊ฐ€ ๊ตฌ์กฐ ์‚ฌ์–‘์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ๊ตฌ์กฐ ์‚ฌ์–‘ ๋งŒ์กฑ ์—ฌ๋ถ€ + */ + override fun isSatisfiedBy(node: ASTNode): Boolean { + return when (node) { + is NumberNode -> isValidNumberStructure(node) + is BooleanNode -> isValidBooleanStructure(node) + is VariableNode -> isValidVariableStructure(node) + is BinaryOpNode -> isValidBinaryOpStructure(node) + is UnaryOpNode -> isValidUnaryOpStructure(node) + is FunctionCallNode -> isValidFunctionCallStructure(node) + is IfNode -> isValidIfStructure(node) + is ArgumentsNode -> isValidArgumentsStructure(node) + else -> false + } + } + + /** + * ๊ตฌ์กฐ ์‚ฌ์–‘์„ ๋งŒ์กฑํ•˜์ง€ ์•Š๋Š” ์ด์œ ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ๊ตฌ์กฐ ์‚ฌ์–‘ ๋ถˆ๋งŒ์กฑ ์ด์œ  + */ + fun getWhyNotSatisfied(node: ASTNode): String { + return when (node) { + is NumberNode -> getNumberStructureViolations(node) + is BooleanNode -> getBooleanStructureViolations(node) + is VariableNode -> getVariableStructureViolations(node) + is BinaryOpNode -> getBinaryOpStructureViolations(node) + is UnaryOpNode -> getUnaryOpStructureViolations(node) + is FunctionCallNode -> getFunctionCallStructureViolations(node) + is IfNode -> getIfStructureViolations(node) + is ArgumentsNode -> getArgumentsStructureViolations(node) + else -> "$UNSUPPORTED_NODE_TYPE ${node::class.simpleName}" + } + } + + /** + * ์ƒ์„ธํ•œ ๊ตฌ์กฐ ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ๊ตฌ์กฐ ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun getStructureValidationResult(node: ASTNode): SpecificationResult { + val isValid = isSatisfiedBy(node) + val violations = mutableListOf() + + if (!isValid) { + violations.add(getWhyNotSatisfied(node)) + } + + // ์ถ”๊ฐ€ ๊ตฌ์กฐ ๊ฒ€์ฆ + violations.addAll(getStructuralIntegrityViolations(node)) + + val finalValid = isValid && violations.isEmpty() + val message = if (finalValid) STRUCTURE_VERIFICATION_SUCCESS else violations.joinToString(", ") + + return SpecificationResult( + success = finalValid, + message = message, + specification = this + ) + } + + // ===== validators ===== + + private fun isValidNumberStructure(node: NumberNode): Boolean { + // ์ˆซ์ž ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ์—ฌ์•ผ ํ•จ + return node.isLeaf() && + node.getChildren().isEmpty() && + node.isLiteral() && + !node.isOperator() && + !node.isFunctionCall() && + !node.isConditional() + } + + private fun isValidBooleanStructure(node: BooleanNode): Boolean { + // ๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ์—ฌ์•ผ ํ•จ + return node.isLeaf() && + node.getChildren().isEmpty() && + node.isLiteral() && + !node.isOperator() && + !node.isFunctionCall() && + !node.isConditional() + } + + private fun isValidVariableStructure(node: VariableNode): Boolean { + // ๋ณ€์ˆ˜ ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ์—ฌ์•ผ ํ•จ + return node.isLeaf() && + node.getChildren().isEmpty() && + !node.isLiteral() && + !node.isOperator() && + !node.isFunctionCall() && + !node.isConditional() + } + + private fun isValidBinaryOpStructure(node: BinaryOpNode): Boolean { + val children = node.getChildren() + + return !node.isLeaf() && + children.size == 2 && + children[0] == node.left && + children[1] == node.right && + !node.isLiteral() && + node.isOperator() && + !node.isFunctionCall() && + !node.isConditional() && + hasValidBinaryOperatorPrecedence(node) && + hasValidBinaryOperatorAssociativity(node) + } + + private fun isValidUnaryOpStructure(node: UnaryOpNode): Boolean { + val children = node.getChildren() + + return !node.isLeaf() && + children.size == 1 && + children[0] == node.operand && + !node.isLiteral() && + node.isOperator() && + !node.isFunctionCall() && + !node.isConditional() && + hasValidUnaryOperatorPrecedence(node) + } + + private fun isValidFunctionCallStructure(node: FunctionCallNode): Boolean { + val children = node.getChildren() + + return !node.isLeaf() && + children.size == node.args.size && + children == node.args && + !node.isLiteral() && + !node.isOperator() && + node.isFunctionCall() && + !node.isConditional() && + hasValidFunctionSignature(node) + } + + private fun isValidIfStructure(node: IfNode): Boolean { + val children = node.getChildren() + + return !node.isLeaf() && + children.size == 3 && + children[0] == node.condition && + children[1] == node.trueValue && + children[2] == node.falseValue && + !node.isLiteral() && + !node.isOperator() && + !node.isFunctionCall() && + node.isConditional() && + hasValidConditionalStructure(node) + } + + private fun isValidArgumentsStructure(node: ArgumentsNode): Boolean { + val children = node.getChildren() + + return children.size == node.arguments.size && + children == node.arguments && + !node.isLiteral() && + !node.isOperator() && + !node.isFunctionCall() && + !node.isConditional() + } + + // ===== structural integrity violations ===== + + private fun getStructuralIntegrityViolations(node: ASTNode): List { + val violations = mutableListOf() + + // ์ˆœํ™˜ ์ฐธ์กฐ ๊ฒ€์ฆ + if (hasCircularReference(node)) { + violations.add(ERR_CIRCULAR_REFERENCE) + } + + // ๊นŠ์ด ์ œํ•œ ๊ฒ€์ฆ + val depth = node.getDepth() + if (depth > MAX_STRUCTURE_DEPTH) { + violations.add(errDepthExceeded(depth, MAX_STRUCTURE_DEPTH)) + } + + // ๋„ˆ๋น„ ์ œํ•œ ๊ฒ€์ฆ + val size = node.getSize() + if (size > MAX_STRUCTURE_SIZE) { + violations.add(errSizeExceeded(size, MAX_STRUCTURE_SIZE)) + } + + // ์ž์‹ ๋…ธ๋“œ ์ผ๊ด€์„ฑ ๊ฒ€์ฆ + violations.addAll(validateChildrenConsistency(node)) + + return violations + } + + // ===== per-node violations ===== + + private fun getNumberStructureViolations(node: NumberNode): String { + val violations = mutableListOf() + + if (!node.isLeaf()) { + violations.add(ERR_NUMBER_NOT_LEAF) + } + if (node.getChildren().isNotEmpty()) { + violations.add(ERR_NUMBER_HAS_CHILDREN) + } + if (!node.isLiteral()) { + violations.add(ERR_NUMBER_NOT_LITERAL) + } + if (node.isOperator()) { + violations.add(ERR_NUMBER_IS_OPERATOR) + } + + return violations.joinToString("; ") + } + + private fun getBooleanStructureViolations(node: BooleanNode): String { + val violations = mutableListOf() + + if (!node.isLeaf()) { + violations.add(ERR_BOOLEAN_NOT_LEAF) + } + if (node.getChildren().isNotEmpty()) { + violations.add(ERR_BOOLEAN_HAS_CHILDREN) + } + if (!node.isLiteral()) { + violations.add(ERR_BOOLEAN_NOT_LITERAL) + } + + return violations.joinToString("; ") + } + + private fun getVariableStructureViolations(node: VariableNode): String { + val violations = mutableListOf() + + if (!node.isLeaf()) { + violations.add(ERR_VARIABLE_NOT_LEAF) + } + if (node.getChildren().isNotEmpty()) { + violations.add(ERR_VARIABLE_HAS_CHILDREN) + } + if (node.isLiteral()) { + violations.add(ERR_VARIABLE_IS_LITERAL) + } + + return violations.joinToString("; ") + } + + private fun getBinaryOpStructureViolations(node: BinaryOpNode): String { + val violations = mutableListOf() + val children = node.getChildren() + + if (node.isLeaf()) { + violations.add(ERR_BINARY_IS_LEAF) + } + if (children.size != 2) { + violations.add(ERR_BINARY_CHILDREN_COUNT) + } + if (!node.isOperator()) { + violations.add(ERR_BINARY_NOT_OPERATOR) + } + if (!hasValidBinaryOperatorPrecedence(node)) { + violations.add(ERR_BINARY_INVALID_PRECEDENCE) + } + + return violations.joinToString("; ") + } + + private fun getUnaryOpStructureViolations(node: UnaryOpNode): String { + val violations = mutableListOf() + val children = node.getChildren() + + if (node.isLeaf()) { + violations.add(ERR_UNARY_IS_LEAF) + } + if (children.size != 1) { + violations.add(ERR_UNARY_CHILDREN_COUNT) + } + if (!node.isOperator()) { + violations.add(ERR_UNARY_NOT_OPERATOR) + } + + return violations.joinToString("; ") + } + + private fun getFunctionCallStructureViolations(node: FunctionCallNode): String { + val violations = mutableListOf() + val children = node.getChildren() + + if (node.isLeaf() && node.args.isNotEmpty()) { + violations.add(ERR_FUNC_LEAF_WITH_ARGS) + } + if (children.size != node.args.size) { + violations.add(ERR_FUNC_CHILDREN_COUNT) + } + if (!node.isFunctionCall()) { + violations.add(ERR_FUNC_NOT_FUNCTION) + } + if (!hasValidFunctionSignature(node)) { + violations.add(ERR_FUNC_INVALID_SIGNATURE) + } + + return violations.joinToString("; ") + } + + private fun getIfStructureViolations(node: IfNode): String { + val violations = mutableListOf() + val children = node.getChildren() + + if (node.isLeaf()) { + violations.add(ERR_IF_IS_LEAF) + } + if (children.size != 3) { + violations.add(ERR_IF_CHILDREN_COUNT) + } + if (!node.isConditional()) { + violations.add(ERR_IF_NOT_CONDITIONAL) + } + if (!hasValidConditionalStructure(node)) { + violations.add(ERR_IF_INVALID_STRUCTURE) + } + + return violations.joinToString("; ") + } + + private fun getArgumentsStructureViolations(node: ArgumentsNode): String { + val violations = mutableListOf() + val children = node.getChildren() + + if (children.size != node.arguments.size) { + violations.add(ERR_ARGS_CHILDREN_COUNT) + } + + return violations.joinToString("; ") + } + + // ===== integrity helpers ===== + + private fun hasValidBinaryOperatorPrecedence(node: BinaryOpNode): Boolean { + return node.getPrecedence() > 0 + } + + private fun hasValidBinaryOperatorAssociativity(node: BinaryOpNode): Boolean { + return node.isLeftAssociative() || node.isRightAssociative() + } + + private fun hasValidUnaryOperatorPrecedence(node: UnaryOpNode): Boolean { + return node.getPrecedence() > 0 + } + + private fun hasValidFunctionSignature(node: FunctionCallNode): Boolean { + return when (node.name.uppercase()) { + "SQRT", "ABS", "SIN", "COS", "TAN", "LOG", "EXP" -> node.args.size == 1 + "POW", "ATAN2", "MAX", "MIN" -> node.args.size >= 1 + "IF" -> node.args.size == 3 + else -> true + } + } + + private fun hasValidConditionalStructure(node: IfNode): Boolean { + // ์กฐ๊ฑด๋ฌธ์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ ๊ฒ€์ฆ + return node.condition != null && + node.trueValue != null && + node.falseValue != null && + node.getNestingDepth() <= MAX_CONDITIONAL_NESTING + } + + private fun hasCircularReference(node: ASTNode): Boolean { + return hasCircularReferenceHelper(node, mutableSetOf()) + } + + /** + * ์ˆœํ™˜ ์ฐธ์กฐ ๊ฒ€์ฆ ํ—ฌํผ + * - ๊ฐ ๊ฒฝ๋กœ๋ณ„ ๋…๋ฆฝ ์ถ”์ ์„ ์œ„ํ•ด path๋ฅผ ๋ณต์‚ฌ + */ + private fun hasCircularReferenceHelper(node: ASTNode, path: MutableSet): Boolean { + if (node in path) { + return true + } + + path.add(node) + return node.getChildren().any { child -> + hasCircularReferenceHelper(child, path.toMutableSet()) + } + } + + /** + * ์ž์‹ ๋…ธ๋“œ ์ผ๊ด€์„ฑ ๊ฒ€์ฆ + */ + private fun validateChildrenConsistency(node: ASTNode): List { + val violations = mutableListOf() + val children = node.getChildren() + + children.forEach { child -> + try { + if (!child.validate()) { + violations.add(errInvalidChildFound(child::class.simpleName ?: UNKNOWN)) + } + } catch (e: Exception) { + violations.add(errChildValidationException(child::class.simpleName ?: UNKNOWN, e.message ?: "")) + } + } + + return violations + } + + companion object { + private const val MAX_STRUCTURE_DEPTH = 100 + private const val MAX_STRUCTURE_SIZE = 10000 + private const val MAX_CONDITIONAL_NESTING = 20 + + // ===== violation message constants ===== + const val ERR_CIRCULAR_REFERENCE = "์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + + const val ERR_NUMBER_NOT_LEAF = "์ˆซ์ž ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_NUMBER_HAS_CHILDREN = "์ˆซ์ž ๋…ธ๋“œ๋Š” ์ž์‹ ๋…ธ๋“œ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val ERR_NUMBER_NOT_LITERAL = "์ˆซ์ž ๋…ธ๋“œ๋Š” ๋ฆฌํ„ฐ๋Ÿด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_NUMBER_IS_OPERATOR = "์ˆซ์ž ๋…ธ๋“œ๋Š” ์—ฐ์‚ฐ์ž๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + + const val ERR_BOOLEAN_NOT_LEAF = "๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_BOOLEAN_HAS_CHILDREN = "๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ๋Š” ์ž์‹ ๋…ธ๋“œ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val ERR_BOOLEAN_NOT_LITERAL = "๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ๋Š” ๋ฆฌํ„ฐ๋Ÿด์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + + const val ERR_VARIABLE_NOT_LEAF = "๋ณ€์ˆ˜ ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_VARIABLE_HAS_CHILDREN = "๋ณ€์ˆ˜ ๋…ธ๋“œ๋Š” ์ž์‹ ๋…ธ๋“œ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val ERR_VARIABLE_IS_LITERAL = "๋ณ€์ˆ˜ ๋…ธ๋“œ๋Š” ๋ฆฌํ„ฐ๋Ÿด์ด ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + + const val ERR_BINARY_IS_LEAF = "์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val ERR_BINARY_CHILDREN_COUNT = "์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋Š” ์ •ํ™•ํžˆ 2๊ฐœ์˜ ์ž์‹ ๋…ธ๋“œ๋ฅผ ๊ฐ€์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_BINARY_NOT_OPERATOR = "์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋Š” ์—ฐ์‚ฐ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_BINARY_INVALID_PRECEDENCE = "์ดํ•ญ ์—ฐ์‚ฐ์ž์˜ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + + const val ERR_UNARY_IS_LEAF = "๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val ERR_UNARY_CHILDREN_COUNT = "๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋Š” ์ •ํ™•ํžˆ 1๊ฐœ์˜ ์ž์‹ ๋…ธ๋“œ๋ฅผ ๊ฐ€์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_UNARY_NOT_OPERATOR = "๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋Š” ์—ฐ์‚ฐ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + + const val ERR_FUNC_LEAF_WITH_ARGS = "์ธ์ˆ˜๊ฐ€ ์žˆ๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val ERR_FUNC_CHILDREN_COUNT = "ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ์˜ ์ž์‹ ๋…ธ๋“œ ์ˆ˜์™€ ์ธ์ˆ˜ ์ˆ˜๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val ERR_FUNC_NOT_FUNCTION = "ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋Š” ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_FUNC_INVALID_SIGNATURE = "ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + + const val ERR_IF_IS_LEAF = "์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ๋Š” ๋ฆฌํ”„ ๋…ธ๋“œ๊ฐ€ ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val ERR_IF_CHILDREN_COUNT = "์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ๋Š” ์ •ํ™•ํžˆ 3๊ฐœ์˜ ์ž์‹ ๋…ธ๋“œ๋ฅผ ๊ฐ€์ ธ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_IF_NOT_CONDITIONAL = "์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ๋Š” ์กฐ๊ฑด๋ฌธ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val ERR_IF_INVALID_STRUCTURE = "์กฐ๊ฑด๋ฌธ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + + const val ERR_ARGS_CHILDREN_COUNT = "์ธ์ˆ˜ ๋ชฉ๋ก ๋…ธ๋“œ์˜ ์ž์‹ ๋…ธ๋“œ ์ˆ˜์™€ ์ธ์ˆ˜ ์ˆ˜๊ฐ€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + + // ๋™์  ๋ฉ”์‹œ์ง€ ๋นŒ๋” + fun errDepthExceeded(actual: Int, max: Int) = + "๋…ธ๋“œ ๊ตฌ์กฐ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค: $actual > $max" + + fun errSizeExceeded(actual: Int, max: Int) = + "๋…ธ๋“œ ๊ตฌ์กฐ ํฌ๊ธฐ๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค: $actual > $max" + + fun errInvalidChildFound(type: String) = + "์œ ํšจํ•˜์ง€ ์•Š์€ ์ž์‹ ๋…ธ๋“œ๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: $type" + + fun errChildValidationException(type: String, message: String) = + "์ž์‹ ๋…ธ๋“œ ๊ฒ€์ฆ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: $type - $message" + + private const val STRUCTURE_VERIFICATION_SUCCESS = "๊ตฌ์กฐ ๊ฒ€์ฆ ์„ฑ๊ณต" + private const val UNSUPPORTED_NODE_TYPE = "์ง€์›๋˜์ง€ ์•Š๋Š” ๋…ธ๋“œ ํƒ€์ž…์ž…๋‹ˆ๋‹ค:" + private const val UNKNOWN = "Unknown" + } + + // SpecificationContract ๊ตฌํ˜„ + override fun getName(): String = "AST ๋…ธ๋“œ ๊ตฌ์กฐ ์‚ฌ์–‘" + override fun getDescription(): String = "AST ๋…ธ๋“œ์˜ ๊ตฌ์กฐ์  ์ •ํ•ฉ์„ฑ๊ณผ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ์‚ฌ์–‘" + override fun getDomain(): String = "ast" + override fun getPriority(): Priority = Priority.NORMAL +} diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/ASTValidationUtils.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/ASTValidationUtils.kt new file mode 100644 index 00000000..e164abbe --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/ASTValidationUtils.kt @@ -0,0 +1,124 @@ +package hs.kr.entrydsm.domain.ast.utils + +import hs.kr.entrydsm.domain.ast.entities.ASTNode + +/** + * AST ๊ฒ€์ฆ์„ ์œ„ํ•œ ๊ณตํ†ต ์œ ํ‹ธ๋ฆฌํ‹ฐ ๋ฉ”์„œ๋“œ๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * ์—ฌ๋Ÿฌ ์ •์ฑ… ํด๋ž˜์Šค์—์„œ ์ค‘๋ณต์œผ๋กœ ์‚ฌ์šฉ๋˜๋Š” ๊ฒ€์ฆ ๋กœ์ง์„ ์ค‘์•™ํ™”ํ•˜์—ฌ + * DRY ์›์น™์„ ์ค€์ˆ˜ํ•˜๊ณ  ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.03 + */ +object ASTValidationUtils { + + /** + * ๋ณ€์ˆ˜๋ช…์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ™•์ธํ•  ๋ณ€์ˆ˜๋ช… + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValidVariableName(name: String): Boolean { + if (name.isEmpty()) return false + if (!name[0].isLetter() && name[0] != '_') return false + return name.drop(1).all { it.isLetterOrDigit() || it == '_' } + } + + /** + * ํ•จ์ˆ˜๋ช…์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ™•์ธํ•  ํ•จ์ˆ˜๋ช… + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValidFunctionName(name: String): Boolean { + if (name.isEmpty()) return false + if (!name[0].isLetter()) return false + return name.drop(1).all { it.isLetterOrDigit() || it == '_' } + } + + /** + * ์˜ˆ์•ฝ์–ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ์˜ˆ์•ฝ์–ด์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isReservedWord(name: String): Boolean { + return RESERVED_WORDS.contains(name.lowercase()) + } + + /** + * ์ง€์›๋˜๋Š” ์ดํ•ญ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ํ™•์ธํ•  ์—ฐ์‚ฐ์ž + * @return ์ง€์›๋˜๋Š” ์ดํ•ญ ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSupportedBinaryOperator(operator: String): Boolean { + return BINARY_OPERATORS.contains(operator) + } + + /** + * ์ง€์›๋˜๋Š” ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ํ™•์ธํ•  ์—ฐ์‚ฐ์ž + * @return ์ง€์›๋˜๋Š” ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSupportedUnaryOperator(operator: String): Boolean { + return UNARY_OPERATORS.contains(operator) + } + + /** + * ๋…ธ๋“œ๊ฐ€ 0 ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ํ™•์ธํ•  AST ๋…ธ๋“œ + * @return 0 ์ƒ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isZeroConstant(node: ASTNode): Boolean { + return node is hs.kr.entrydsm.domain.ast.entities.NumberNode && node.value == 0.0 + } + + /** + * ์˜ˆ์•ฝ์–ด ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ˆ์•ฝ์–ด ์ง‘ํ•ฉ + */ + fun getReservedWords(): Set = RESERVED_WORDS.toSet() + + /** + * ์ง€์›๋˜๋Š” ์ดํ•ญ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ดํ•ญ ์—ฐ์‚ฐ์ž ์ง‘ํ•ฉ + */ + fun getBinaryOperators(): Set = BINARY_OPERATORS.toSet() + + /** + * ์ง€์›๋˜๋Š” ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ์ง‘ํ•ฉ + */ + fun getUnaryOperators(): Set = UNARY_OPERATORS.toSet() + + /** + * ์˜ˆ์•ฝ์–ด ๋ชฉ๋ก + */ + private val RESERVED_WORDS = setOf( + "if", "else", "while", "for", "do", "break", "continue", + "function", "return", "var", "let", "const", "true", "false", + "null", "undefined", "this", "new", "typeof", "instanceof", + "try", "catch", "finally", "throw", "switch", "case", "default" + ) + + /** + * ์ง€์›๋˜๋Š” ์ดํ•ญ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก + */ + private val BINARY_OPERATORS = setOf( + "+", "-", "*", "/", "%", "^", + "==", "!=", "<", "<=", ">", ">=", + "&&", "||" + ) + + /** + * ์ง€์›๋˜๋Š” ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋ชฉ๋ก + */ + private val UNARY_OPERATORS = setOf("-", "+", "!") +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/FunctionValidationRules.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/FunctionValidationRules.kt new file mode 100644 index 00000000..158eaf1c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/utils/FunctionValidationRules.kt @@ -0,0 +1,152 @@ +package hs.kr.entrydsm.domain.ast.utils + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.exceptions.ASTException + +/** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฒ€์ฆ ๊ทœ์น™์„ ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ํ•˜๋“œ์ฝ”๋”ฉ๋œ when ๊ตฌ๋ฌธ์„ Map ๊ธฐ๋ฐ˜ ๊ตฌ์กฐ๋กœ ๋Œ€์ฒดํ•˜์—ฌ ํ™•์žฅ์„ฑ๊ณผ ์œ ์ง€๋ณด์ˆ˜์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.03 + */ +object FunctionValidationRules { + + /** + * ํ•จ์ˆ˜ ๊ฒ€์ฆ ๊ทœ์น™ ์ธํ„ฐํŽ˜์ด์Šค + */ + fun interface ValidationRule { + fun validate(args: List): Boolean + } + + /** + * ํ•จ์ˆ˜๋ณ„ ๊ฒ€์ฆ ๊ทœ์น™ ๋งต + */ + private val VALIDATION_RULES = mapOf( + // ์ •ํ™•ํ•œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ํ•„์š”ํ•œ ํ•จ์ˆ˜๋“ค + "SQRT" to ValidationRule { args -> args.size == 1 }, + "POW" to ValidationRule { args -> args.size == 2 }, + "IF" to ValidationRule { args -> args.size == 3 }, + + // ๋‹จ์ผ ์ธ์ˆ˜ ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค + "SIN" to ValidationRule { args -> args.size == 1 }, + "COS" to ValidationRule { args -> args.size == 1 }, + "TAN" to ValidationRule { args -> args.size == 1 }, + "ABS" to ValidationRule { args -> args.size == 1 }, + "LOG" to ValidationRule { args -> args.size == 1 }, + "EXP" to ValidationRule { args -> args.size == 1 }, + + // ๊ฐ€๋ณ€ ์ธ์ˆ˜ ํ•จ์ˆ˜๋“ค (์ตœ์†Œ 1๊ฐœ) + "MAX" to ValidationRule { args -> args.isNotEmpty() }, + "MIN" to ValidationRule { args -> args.isNotEmpty() } + ) + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @param args ์ธ์ˆ˜ ๋ชฉ๋ก + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValidFunctionCall(name: String, args: List): Boolean { + val rule = VALIDATION_RULES[name.uppercase()] + return rule?.validate(args) ?: true // ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ํ•จ์ˆ˜๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ํ—ˆ์šฉ + } + + /** + * ํ•จ์ˆ˜๋ณ„ ํ•„์š”ํ•œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (๋””๋ฒ„๊น…์šฉ) + * + * @param name ํ•จ์ˆ˜๋ช… + * @return ์ธ์ˆ˜ ๊ฐœ์ˆ˜ ์„ค๋ช… ๋ฌธ์ž์—ด + */ + fun getArgumentCountDescription(name: String): String { + return when (name.uppercase()) { + "SQRT", "SIN", "COS", "TAN", "ABS", "LOG", "EXP" -> "์ •ํ™•ํžˆ 1๊ฐœ" + "POW" -> "์ •ํ™•ํžˆ 2๊ฐœ" + "IF" -> "์ •ํ™•ํžˆ 3๊ฐœ" + "MAX", "MIN" -> "์ตœ์†Œ 1๊ฐœ" + else -> "์ œํ•œ ์—†์Œ" + } + } + + /** + * ํ•จ์ˆ˜์˜ ์˜ˆ์ƒ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @return ์˜ˆ์ƒ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ (๊ฐ€๋ณ€ ์ธ์ˆ˜์ธ ๊ฒฝ์šฐ -1) + */ + fun getExpectedArgumentCount(name: String): Int { + return when (name.uppercase()) { + "SQRT", "SIN", "COS", "TAN", "ABS", "LOG", "EXP" -> 1 + "POW" -> 2 + "IF" -> 3 + "MAX", "MIN" -> -1 // ๊ฐ€๋ณ€ ์ธ์ˆ˜ + else -> -1 // ์•Œ ์ˆ˜ ์—†์Œ + } + } + + /** + * ๋“ฑ๋ก๋œ ๋ชจ๋“  ํ•จ์ˆ˜๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋“ฑ๋ก๋œ ํ•จ์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + fun getRegisteredFunctions(): Set { + return VALIDATION_RULES.keys + } + + /** + * ํ•จ์ˆ˜๊ฐ€ ๋“ฑ๋ก๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @return ๋“ฑ๋ก๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isRegisteredFunction(name: String): Boolean { + return VALIDATION_RULES.containsKey(name.uppercase()) + } + + /** + * ์ƒˆ๋กœ์šด ํ•จ์ˆ˜ ๊ฒ€์ฆ ๊ทœ์น™์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. (ํ™•์žฅ์„ฑ์„ ์œ„ํ•œ ๋ฉ”์„œ๋“œ) + * + * @param name ํ•จ์ˆ˜๋ช… + * @param rule ๊ฒ€์ฆ ๊ทœ์น™ + */ + fun addValidationRule(name: String, rule: ValidationRule) { + // ๋Ÿฐํƒ€์ž„์— ๊ทœ์น™์„ ์ถ”๊ฐ€ํ•  ์ˆ˜ ์žˆ๋„๋ก MutableMap์œผ๋กœ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅ + // ํ˜„์žฌ๋Š” ์ฝ๊ธฐ ์ „์šฉ์œผ๋กœ ์„ค๊ณ„๋จ + throw ASTException.runtimeRuleNotSupported() + } + + /** + * ํ•จ์ˆ˜ ๊ฒ€์ฆ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getValidationStatistics(): Map { + val exactArgFunctions = VALIDATION_RULES.filterValues { rule -> + // ์ •ํ™•ํ•œ ๊ฐœ์ˆ˜๋ฅผ ์š”๊ตฌํ•˜๋Š” ํ•จ์ˆ˜๋“ค (๊ฐ„๋‹จํ•œ ํœด๋ฆฌ์Šคํ‹ฑ) + listOf(1, 2, 3).any { count -> + rule.validate(List(count) { createDummyNode() }) + } + } + + val variableArgFunctions = VALIDATION_RULES.filterValues { rule -> + // ๊ฐ€๋ณ€ ์ธ์ˆ˜ ํ•จ์ˆ˜๋“ค + rule.validate(listOf(createDummyNode())) && rule.validate(listOf(createDummyNode(), createDummyNode())) + } + + return mapOf( + "totalFunctions" to VALIDATION_RULES.size, + "exactArgFunctions" to exactArgFunctions.size, + "variableArgFunctions" to variableArgFunctions.size, + "registeredFunctions" to getRegisteredFunctions() + ) + } + + /** + * ํ…Œ์ŠคํŠธ์šฉ ๋”๋ฏธ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private fun createDummyNode(): ASTNode { + return hs.kr.entrydsm.domain.ast.entities.NumberNode(0.0) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTOptimizationResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTOptimizationResult.kt new file mode 100644 index 00000000..90be86af --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTOptimizationResult.kt @@ -0,0 +1,24 @@ +package hs.kr.entrydsm.domain.ast.values + +import java.time.LocalDateTime + +/** + * ์ตœ์ ํ™” ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + */ +data class ASTOptimizationResult( + val originalSize: NodeSize, + val optimizedSize: NodeSize, + val originalDepth: TreeDepth, + val optimizedDepth: TreeDepth, + val level: OptimizationLevel, + val optimizedAt: LocalDateTime, + val astId: String +) { + fun getReductionRatio(): Double { + return if (originalSize.value > 0) { + (originalSize.value - optimizedSize.value).toDouble() / originalSize.value.toDouble() + } else { + 0.0 + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTValidationResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTValidationResult.kt new file mode 100644 index 00000000..13b295ab --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/ASTValidationResult.kt @@ -0,0 +1,13 @@ +package hs.kr.entrydsm.domain.ast.values + +import java.time.LocalDateTime + +/** + * ๊ฒ€์ฆ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + */ +data class ASTValidationResult( + val isValid: Boolean, + val violations: List, + val validatedAt: LocalDateTime, + val astId: String +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeSize.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeSize.kt new file mode 100644 index 00000000..8a1b993b --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeSize.kt @@ -0,0 +1,298 @@ +package hs.kr.entrydsm.domain.ast.values + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException + +/** + * AST ๋…ธ๋“œ์˜ ํฌ๊ธฐ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ๋…ธ๋“œ์˜ ํฌ๊ธฐ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋ฉฐ, ํฌ๊ธฐ ์ œํ•œ๊ณผ + * ๊ด€๋ จ๋œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +data class NodeSize private constructor(val value: Int) { + + init { + if (value < 0) { + throw ASTException.nodeSizeNegative(value) + } + if (value > MAX_SIZE) { + throw ASTException.nodeSizeTooLarge(value, MAX_SIZE) + } + + } + + /** + * ํฌ๊ธฐ๋ฅผ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. + */ + fun increment(): NodeSize { + return of(value + 1) + } + + /** + * ํฌ๊ธฐ๋ฅผ ๊ฐ์†Œ์‹œํ‚ต๋‹ˆ๋‹ค. + */ + fun decrement(): NodeSize { + return if (value > 0) of(value - 1) else this + } + + /** + * ์ง€์ •๋œ ๊ฐ’๋งŒํผ ํฌ๊ธฐ๋ฅผ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. + */ + fun plus(amount: Int): NodeSize { + return of(value + amount) + } + + /** + * ์ง€์ •๋œ ๊ฐ’๋งŒํผ ํฌ๊ธฐ๋ฅผ ๊ฐ์†Œ์‹œํ‚ต๋‹ˆ๋‹ค. + */ + fun minus(amount: Int): NodeSize { + return of(maxOf(0, value - amount)) + } + + /** + * ๋‹ค๋ฅธ ํฌ๊ธฐ์™€ ๋”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun plus(other: NodeSize): NodeSize { + return of(value + other.value) + } + + /** + * ๋‹ค๋ฅธ ํฌ๊ธฐ์™€ ๋บ๋‹ˆ๋‹ค. + */ + fun minus(other: NodeSize): NodeSize { + return of(maxOf(0, value - other.value)) + } + + /** + * ๋‹ค๋ฅธ ํฌ๊ธฐ์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isGreaterThan(other: NodeSize): Boolean = value > other.value + + /** + * ๋‹ค๋ฅธ ํฌ๊ธฐ์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isLessThan(other: NodeSize): Boolean = value < other.value + + /** + * ๋‹ค๋ฅธ ํฌ๊ธฐ์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isGreaterOrEqual(other: NodeSize): Boolean = value >= other.value + + /** + * ๋‹ค๋ฅธ ํฌ๊ธฐ์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isLessOrEqual(other: NodeSize): Boolean = value <= other.value + + /** + * ๋นˆ ํฌ๊ธฐ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isEmpty(): Boolean = value == 0 + + /** + * ๋‹จ์ผ ๋…ธ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isSingle(): Boolean = value == 1 + + /** + * ์ž‘์€ ํฌ๊ธฐ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isSmall(): Boolean = value <= SMALL_SIZE + + /** + * ์ค‘๊ฐ„ ํฌ๊ธฐ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isMedium(): Boolean = value in (SMALL_SIZE + 1)..MEDIUM_SIZE + + /** + * ํฐ ํฌ๊ธฐ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isLarge(): Boolean = value in (MEDIUM_SIZE + 1)..LARGE_SIZE + + /** + * ๋งค์šฐ ํฐ ํฌ๊ธฐ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isVeryLarge(): Boolean = value > LARGE_SIZE + + /** + * ์ตœ๋Œ€ ํฌ๊ธฐ์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isAtLimit(): Boolean = value >= MAX_SIZE + + /** + * ๊ฒฝ๊ณ  ์ˆ˜์ค€์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isWarningLevel(): Boolean = value >= WARNING_SIZE + + /** + * ๋‘ ํฌ๊ธฐ ์ค‘ ์ตœ๋Œ€๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun max(other: NodeSize): NodeSize { + return if (value >= other.value) this else other + } + + /** + * ๋‘ ํฌ๊ธฐ ์ค‘ ์ตœ์†Œ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun min(other: NodeSize): NodeSize { + return if (value <= other.value) this else other + } + + /** + * ๋ฐฑ๋ถ„์œจ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun toPercentage(): Double = (value.toDouble() / MAX_SIZE) * 100 + + /** + * ํฌ๊ธฐ ๋ ˆ๋ฒจ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getLevel(): SizeLevel { + return when { + value == 0 -> SizeLevel.EMPTY + value <= SMALL_SIZE -> SizeLevel.SMALL + value <= MEDIUM_SIZE -> SizeLevel.MEDIUM + value <= LARGE_SIZE -> SizeLevel.LARGE + value <= WARNING_SIZE -> SizeLevel.VERY_LARGE + value > WARNING_SIZE -> SizeLevel.CRITICAL + else -> SizeLevel.EMPTY + } + } + + /** + * ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค (๋ฐ”์ดํŠธ ๋‹จ์œ„). + */ + fun estimateMemoryUsage(): Long { + return value * BYTES_PER_NODE + } + + /** + * ๋ฌธ์ž์—ด ํ‘œํ˜„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun toString(): String = "NodeSize($value)" + + /** + * ํฌ๊ธฐ ๋ ˆ๋ฒจ์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜• + */ + enum class SizeLevel(val description: String) { + EMPTY("๋นˆ ํฌ๊ธฐ"), + SMALL("์ž‘์€ ํฌ๊ธฐ"), + MEDIUM("์ค‘๊ฐ„ ํฌ๊ธฐ"), + LARGE("ํฐ ํฌ๊ธฐ"), + VERY_LARGE("๋งค์šฐ ํฐ ํฌ๊ธฐ"), + CRITICAL("์œ„ํ—˜ ํฌ๊ธฐ") + } + + companion object { + private const val MAX_SIZE = 10000 + private const val WARNING_SIZE = 8000 + private const val LARGE_SIZE = 1000 + private const val MEDIUM_SIZE = 100 + private const val SMALL_SIZE = 10 + private const val BYTES_PER_NODE = 64L + + private val ZERO = NodeSize(0) + private val ONE = NodeSize(1) + + /** + * ๊ฐ’์œผ๋กœ NodeSize๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun of(value: Int): NodeSize { + return when (value) { + 0 -> ZERO + 1 -> ONE + else -> NodeSize(value) + } + } + + /** + * 0 ํฌ๊ธฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun zero(): NodeSize = ZERO + + /** + * 1 ํฌ๊ธฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun one(): NodeSize = ONE + + /** + * ์ตœ๋Œ€ ํฌ๊ธฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun max(): NodeSize = of(MAX_SIZE) + + /** + * ๊ฒฝ๊ณ  ํฌ๊ธฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun warning(): NodeSize = of(WARNING_SIZE) + + /** + * ์ž‘์€ ํฌ๊ธฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun small(): NodeSize = of(SMALL_SIZE) + + /** + * ์ค‘๊ฐ„ ํฌ๊ธฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun medium(): NodeSize = of(MEDIUM_SIZE) + + /** + * ํฐ ํฌ๊ธฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun large(): NodeSize = of(LARGE_SIZE) + + /** + * ์—ฌ๋Ÿฌ ํฌ๊ธฐ ์ค‘ ์ตœ๋Œ€๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun maxOf(vararg sizes: NodeSize): NodeSize { + return sizes.maxByOrNull { it.value } ?: ZERO + } + + /** + * ์—ฌ๋Ÿฌ ํฌ๊ธฐ ์ค‘ ์ตœ์†Œ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun minOf(vararg sizes: NodeSize): NodeSize { + return sizes.minByOrNull { it.value } ?: ZERO + } + + /** + * ์—ฌ๋Ÿฌ ํฌ๊ธฐ์˜ ํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun sum(sizes: List): NodeSize { + if (sizes.isEmpty()) return ZERO + val total = sizes.sumOf { it.value } + return of(total) + } + + /** + * ํ‰๊ท  ํฌ๊ธฐ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun average(sizes: List): NodeSize { + if (sizes.isEmpty()) return ZERO + val avg = sizes.map { it.value }.average().toInt() + return of(avg) + } + + /** + * ์œ ํšจํ•œ ํฌ๊ธฐ ๋ฒ”์œ„์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isValidRange(size: Int): Boolean { + return size in 0..MAX_SIZE + } + + /** + * ์•ˆ์ „ํ•œ ํฌ๊ธฐ ์ƒ์„ฑ (๋ฒ”์œ„ ๊ฒ€์ฆ ์—†์Œ) + */ + fun tryOf(value: Int): NodeSize? { + return if (isValidRange(value)) of(value) else null + } + + /** + * ์ด ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun calculateTotalMemoryUsage(sizes: List): Long { + return sizes.sumOf { it.estimateMemoryUsage() } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeType.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeType.kt new file mode 100644 index 00000000..f8232892 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/NodeType.kt @@ -0,0 +1,166 @@ +package hs.kr.entrydsm.domain.ast.values + +/** + * AST ๋…ธ๋“œ์˜ ํƒ€์ž…์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * AST ๋…ธ๋“œ์˜ ์ข…๋ฅ˜๋ฅผ ๊ตฌ๋ถ„ํ•˜๋ฉฐ, ๊ฐ ๋…ธ๋“œ ํƒ€์ž…๋ณ„ ํŠน์„ฑ๊ณผ + * ์ฒ˜๋ฆฌ ๋ฐฉ๋ฒ•์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +enum class NodeType( + val description: String, + val isLeaf: Boolean, + val isOperator: Boolean, + val isLiteral: Boolean, + val priority: Int +) { + + /** + * ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด ๋…ธ๋“œ + */ + NUMBER("์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด", true, false, true, 0), + + /** + * ๋ถˆ๋ฆฌ์–ธ ๋ฆฌํ„ฐ๋Ÿด ๋…ธ๋“œ + */ + BOOLEAN("๋ถˆ๋ฆฌ์–ธ ๋ฆฌํ„ฐ๋Ÿด", true, false, true, 0), + + /** + * ๋ณ€์ˆ˜ ๋…ธ๋“œ + */ + VARIABLE("๋ณ€์ˆ˜", true, false, false, 0), + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ์ž ๋…ธ๋“œ + */ + BINARY_OP("์ดํ•ญ ์—ฐ์‚ฐ์ž", false, true, false, 2), + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋…ธ๋“œ + */ + UNARY_OP("๋‹จํ•ญ ์—ฐ์‚ฐ์ž", false, true, false, 1), + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ + */ + FUNCTION_CALL("ํ•จ์ˆ˜ ํ˜ธ์ถœ", false, false, false, 3), + + /** + * ์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ + */ + IF("์กฐ๊ฑด๋ฌธ", false, false, false, 4), + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก ๋…ธ๋“œ + */ + ARGUMENTS("์ธ์ˆ˜ ๋ชฉ๋ก", false, false, false, 1); + + /** + * ๋ฆฌํ„ฐ๋Ÿด ๋…ธ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isLiteralNode(): Boolean = isLiteral + + /** + * ์—ฐ์‚ฐ์ž ๋…ธ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isOperatorNode(): Boolean = isOperator + + /** + * ๋ฆฌํ”„ ๋…ธ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isLeafNode(): Boolean = isLeaf + + /** + * ๋ณตํ•ฉ ๋…ธ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isComplexNode(): Boolean = !isLeaf + + + /** + * ํŠน์ • ๋…ธ๋“œ ํƒ€์ž…๊ณผ ํ˜ธํ™˜๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isCompatibleWith(other: NodeType): Boolean = when { + this.isLiteral && other.isLiteral -> true + this.isOperator && other.isOperator -> true + (this == VARIABLE && other.isLiteral) || (this.isLiteral && other == VARIABLE) -> true + else -> false + } + + /** + * ๋…ธ๋“œ ํƒ€์ž…๋ณ„ ์˜ˆ์ƒ ์ž์‹ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getExpectedChildCount(): IntRange { + return when (this) { + NUMBER, BOOLEAN, VARIABLE -> 0..0 + UNARY_OP -> 1..1 + BINARY_OP -> 2..2 + IF -> 3..3 + FUNCTION_CALL -> 0..UNLIMITED_CHILD_COUNT + ARGUMENTS -> 0..UNLIMITED_CHILD_COUNT + } + } + + /** + * ๋…ธ๋“œ ํƒ€์ž…๋ณ„ ์ตœ๋Œ€ ๊นŠ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getMaxDepth(): Int { + return when (this) { + NUMBER, BOOLEAN, VARIABLE -> 1 + UNARY_OP -> 10 + BINARY_OP -> 15 + FUNCTION_CALL -> 20 + IF -> 25 + ARGUMENTS -> 5 + } + } + + companion object { + private const val UNLIMITED_CHILD_COUNT = Int.MAX_VALUE + + /** + * ๋ชจ๋“  ๋ฆฌํ„ฐ๋Ÿด ๋…ธ๋“œ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + val literalTypes: Set by lazy { + entries.filter { it.isLiteral }.toSet() + } + + /** + * ๋ชจ๋“  ์—ฐ์‚ฐ์ž ๋…ธ๋“œ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + val operatorTypes: Set by lazy { + entries.filter { it.isOperator }.toSet() + } + + /** + * ๋ชจ๋“  ๋ฆฌํ”„ ๋…ธ๋“œ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + val leafTypes: Set by lazy { + entries.filter { it.isLeaf }.toSet() + } + + /** + * ๋ชจ๋“  ๋ณตํ•ฉ ๋…ธ๋“œ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + val complexTypes: Set by lazy { + entries.filter { !it.isLeaf }.toSet() + } + + /** + * ์šฐ์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ ๋…ธ๋“œ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + val sortedByPriority: List by lazy { + entries.sortedBy { it.priority } + } + + /** + * ์„ค๋ช…์œผ๋กœ ๋…ธ๋“œ ํƒ€์ž…์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + fun findByDescription(description: String): NodeType? { + return entries.find { it.description == description } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/OptimizationLevel.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/OptimizationLevel.kt new file mode 100644 index 00000000..4f53095c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/OptimizationLevel.kt @@ -0,0 +1,10 @@ +package hs.kr.entrydsm.domain.ast.values + +/** + * ์ตœ์ ํ™” ๋ ˆ๋ฒจ ์—ด๊ฑฐํ˜• + */ +enum class OptimizationLevel(val description: String) { + NONE("์ตœ์ ํ™” ์—†์Œ"), + BASIC("๊ธฐ๋ณธ ์ตœ์ ํ™”"), + FULL("์™„์ „ ์ตœ์ ํ™”") +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeDepth.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeDepth.kt new file mode 100644 index 00000000..6edc85b5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeDepth.kt @@ -0,0 +1,222 @@ +package hs.kr.entrydsm.domain.ast.values + +import hs.kr.entrydsm.domain.ast.exceptions.ASTException + +/** + * AST ํŠธ๋ฆฌ์˜ ๊นŠ์ด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ํŠธ๋ฆฌ์˜ ๊นŠ์ด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋ฉฐ, ๊นŠ์ด ์ œํ•œ๊ณผ + * ๊ด€๋ จ๋œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +data class TreeDepth private constructor(val value: Int) { + + init { + if (value < 0) { + throw ASTException.treeDepthNegative(value) + } + + if (value > MAX_DEPTH) { + throw ASTException.treeDepthTooLarge(value, MAX_DEPTH) + } + } + + /** + * ๊นŠ์ด๋ฅผ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. + */ + fun increment(): TreeDepth { + return if (value < MAX_DEPTH) of(value + 1) else this + } + + /** + * ๊นŠ์ด๋ฅผ ๊ฐ์†Œ์‹œํ‚ต๋‹ˆ๋‹ค. + */ + fun decrement(): TreeDepth { + return if (value > 0) of(value - 1) else this + } + + /** + * ์ง€์ •๋œ ๊ฐ’๋งŒํผ ๊นŠ์ด๋ฅผ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. + */ + fun plus(amount: Int): TreeDepth { + return of(value + amount) + } + + /** + * ์ง€์ •๋œ ๊ฐ’๋งŒํผ ๊นŠ์ด๋ฅผ ๊ฐ์†Œ์‹œํ‚ต๋‹ˆ๋‹ค. + */ + fun minus(amount: Int): TreeDepth { + return of(maxOf(0, value - amount)) + } + + /** + * ๋‹ค๋ฅธ ๊นŠ์ด์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isGreaterThan(other: TreeDepth): Boolean = value > other.value + + /** + * ๋‹ค๋ฅธ ๊นŠ์ด์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isLessThan(other: TreeDepth): Boolean = value < other.value + + /** + * ๋‹ค๋ฅธ ๊นŠ์ด์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isGreaterOrEqual(other: TreeDepth): Boolean = value >= other.value + + /** + * ๋‹ค๋ฅธ ๊นŠ์ด์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isLessOrEqual(other: TreeDepth): Boolean = value <= other.value + + /** + * ์ตœ๋Œ€ ๊นŠ์ด์™€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + */ + fun isAtLimit(): Boolean = value >= MAX_DEPTH + + /** + * ๊ฒฝ๊ณ  ์ˆ˜์ค€์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isWarningLevel(): Boolean = value >= WARNING_DEPTH + + /** + * ์–•์€ ๊นŠ์ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isShallow(): Boolean = value <= SHALLOW_DEPTH + + /** + * ๊นŠ์€ ๊นŠ์ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isDeep(): Boolean = value >= DEEP_DEPTH + + /** + * ๋‘ ๊นŠ์ด ์ค‘ ์ตœ๋Œ€๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun max(other: TreeDepth): TreeDepth { + return if (value >= other.value) this else other + } + + /** + * ๋‘ ๊นŠ์ด ์ค‘ ์ตœ์†Œ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun min(other: TreeDepth): TreeDepth { + return if (value <= other.value) this else other + } + + /** + * ๋ฐฑ๋ถ„์œจ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun toPercentage(): Double = (value.toDouble() / MAX_DEPTH) * 100 + + /** + * ๊นŠ์ด ๋ ˆ๋ฒจ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getLevel(): DepthLevel { + return when { + value <= SHALLOW_DEPTH -> DepthLevel.SHALLOW + value <= NORMAL_DEPTH -> DepthLevel.NORMAL + value <= DEEP_DEPTH -> DepthLevel.DEEP + value <= WARNING_DEPTH -> DepthLevel.WARNING + else -> DepthLevel.CRITICAL + } + } + + /** + * ๋ฌธ์ž์—ด ํ‘œํ˜„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun toString(): String = "TreeDepth($value)" + + /** + * ๊นŠ์ด ๋ ˆ๋ฒจ์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜• + */ + enum class DepthLevel(val description: String) { + SHALLOW("์–•์€ ๊นŠ์ด"), + NORMAL("์ผ๋ฐ˜ ๊นŠ์ด"), + DEEP("๊นŠ์€ ๊นŠ์ด"), + WARNING("๊ฒฝ๊ณ  ๊นŠ์ด"), + CRITICAL("์œ„ํ—˜ ๊นŠ์ด") + } + + companion object { + private const val MAX_DEPTH = 100 + private const val WARNING_DEPTH = 80 + private const val DEEP_DEPTH = 60 + private const val NORMAL_DEPTH = 40 + private const val SHALLOW_DEPTH = 10 + + private val ZERO = TreeDepth(0) + private val ONE = TreeDepth(1) + + /** + * ๊ฐ’์œผ๋กœ TreeDepth๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun of(value: Int): TreeDepth { + return when (value) { + 0 -> ZERO + 1 -> ONE + else -> TreeDepth(value) + } + } + + /** + * 0 ๊นŠ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun zero(): TreeDepth = ZERO + + /** + * 1 ๊นŠ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun one(): TreeDepth = ONE + + /** + * ์ตœ๋Œ€ ๊นŠ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun max(): TreeDepth = of(MAX_DEPTH) + + /** + * ๊ฒฝ๊ณ  ๊นŠ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun warning(): TreeDepth = of(WARNING_DEPTH) + + /** + * ์—ฌ๋Ÿฌ ๊นŠ์ด ์ค‘ ์ตœ๋Œ€๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun maxOf(vararg depths: TreeDepth): TreeDepth { + return depths.maxByOrNull { it.value } ?: ZERO + } + + /** + * ์—ฌ๋Ÿฌ ๊นŠ์ด ์ค‘ ์ตœ์†Œ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun minOf(vararg depths: TreeDepth): TreeDepth { + return depths.minByOrNull { it.value } ?: ZERO + } + + /** + * ํ‰๊ท  ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun average(depths: List): TreeDepth { + if (depths.isEmpty()) return ZERO + val avg = depths.map { it.value }.average().toInt() + return of(avg) + } + + /** + * ์œ ํšจํ•œ ๊นŠ์ด ๋ฒ”์œ„์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isValidRange(depth: Int): Boolean { + return depth in 0..MAX_DEPTH + } + + /** + * ์•ˆ์ „ํ•œ ๊นŠ์ด ์ƒ์„ฑ (๋ฒ”์œ„ ๊ฒ€์ฆ ์—†์Œ) + */ + fun tryOf(value: Int): TreeDepth? { + return if (isValidRange(value)) of(value) else null + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeStatistics.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeStatistics.kt new file mode 100644 index 00000000..42ef18dd --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/ast/values/TreeStatistics.kt @@ -0,0 +1,45 @@ +package hs.kr.entrydsm.domain.ast.values + +import java.time.LocalDateTime + +/** + * ํŠธ๋ฆฌ ํ†ต๊ณ„ ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + */ +data class TreeStatistics( + val nodeCount: NodeSize, + val leafCount: NodeSize, + val maxDepth: TreeDepth, + val averageDepth: TreeDepth, + val nodeTypeCounts: Map, + val variables: Set, + val astId: String, + val calculatedAt: LocalDateTime +) { + /** + * ๊ฐ€์žฅ ๋งŽ์€ ๋…ธ๋“œ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getMostCommonNodeType(): NodeType? { + return nodeTypeCounts.maxByOrNull { it.value }?.key + } + + /** + * ํŠน์ • ๋…ธ๋“œ ํƒ€์ž…์˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getNodeCount(type: NodeType): Int { + return nodeTypeCounts[type] ?: 0 + } + + /** + * ๋ฆฌํ”„ ๋…ธ๋“œ๋“ค์˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getLeafNodeCount(): Int { + return NodeType.leafTypes.sumOf { getNodeCount(it) } + } + + /** + * ์—ฐ์‚ฐ์ž ๋…ธ๋“œ๋“ค์˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getOperatorNodeCount(): Int { + return NodeType.operatorTypes.sumOf { getNodeCount(it) } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/aggregates/Calculator.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/aggregates/Calculator.kt new file mode 100644 index 00000000..a98953f4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/aggregates/Calculator.kt @@ -0,0 +1,291 @@ +package hs.kr.entrydsm.domain.calculator.aggregates + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import hs.kr.entrydsm.domain.calculator.values.CalculationResult +import hs.kr.entrydsm.domain.evaluator.aggregates.ExpressionEvaluator +import hs.kr.entrydsm.domain.lexer.aggregates.LexerAggregate +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.aggregates.LRParser +import hs.kr.entrydsm.domain.parser.values.Grammar +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import kotlin.system.measureTimeMillis + +/** + * ๊ณ„์‚ฐ๊ธฐ ๋„๋ฉ”์ธ์˜ ํ•ต์‹ฌ ์ง‘ํ•ฉ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค. + * + * ์ˆ˜์‹ ๊ณ„์‚ฐ์˜ ์ „์ฒด ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ด€๋ฆฌํ•˜๋ฉฐ, ๋ ‰์‹ฑ, ํŒŒ์‹ฑ, ํ‰๊ฐ€์˜ ๋ชจ๋“  ๋‹จ๊ณ„๋ฅผ + * ์กฐ์œจํ•ฉ๋‹ˆ๋‹ค. ๋‹ค๋ฅธ ๋„๋ฉ”์ธ๋“ค๊ณผ์˜ ํ˜‘๋ ฅ์„ ํ†ตํ•ด ์™„์ „ํ•œ ๊ณ„์‚ฐ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•˜๋ฉฐ, + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ, ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ, ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ ๋“ฑ์˜ ๊ณ ๊ธ‰ ๊ธฐ๋Šฅ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * @property lexer ํ† ํฐํ™”๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๋ ‰์„œ + * @property maxFormulaLength ์ตœ๋Œ€ ์ˆ˜์‹ ๊ธธ์ด + * @property maxVariables ์ตœ๋Œ€ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Aggregate(context = "calculator") +class Calculator( + private val lexer: LexerAggregate = LexerAggregate(), + private val parser: LRParser = LRParser.createDefault(), + private val maxFormulaLength: Int = 5000, + private val maxVariables: Int = 100 +) { + + /** + * ๋‹จ์ผ ์ˆ˜์‹์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + * @throws CalculatorException ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun calculate(request: CalculationRequest): CalculationResult { + validateRequest(request) + + var result: Any? = null + var ast: ASTNode? = null + var tokens: List? = null + + val executionTime = measureTimeMillis { + try { + // 1. ํ† ํฐํ™” + tokens = tokenize(request.formula) + + // 2. ํŒŒ์‹ฑ (๊ฐ„๋‹จํ•œ ํŒŒ์‹ฑ ๋กœ์ง - ์‹ค์ œ๋กœ๋Š” LRParser ์‚ฌ์šฉ) + ast = parseTokens(tokens!!) + + // 3. ํ‰๊ฐ€ + val evaluator = ExpressionEvaluator.create(request.variables) + result = evaluator.evaluate(ast!!) + + } catch (e: Exception) { + throw CalculatorException.stepExecutionError(1, e) + } + } + + return CalculationResult( + result = result, + executionTimeMs = executionTime, + formula = request.formula, + variables = request.variables, + steps = listOf("ํ† ํฐํ™”", "ํŒŒ์‹ฑ", "ํ‰๊ฐ€"), + ast = ast + ) + } + + /** + * ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param formulas ๊ณ„์‚ฐํ•  ์ˆ˜์‹๋“ค + * @param variables ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ + * @return ๊ฐ ๋‹จ๊ณ„์˜ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + * @throws CalculatorException ๊ณ„์‚ฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun calculateMultiStep(formulas: List, variables: Map = emptyMap()): List { + if (formulas.isEmpty()) { + throw CalculatorException.emptySteps() + } + + if (formulas.size > 50) { // ์ตœ๋Œ€ ๋‹จ๊ณ„ ์ˆ˜ ์ œํ•œ + throw CalculatorException.tooManySteps(formulas.size, 50) + } + + val results = mutableListOf() + var currentVariables = variables.toMutableMap() + + formulas.forEachIndexed { index, formula -> + try { + val request = CalculationRequest(formula, currentVariables) + val result = calculate(request) + results.add(result) + + currentVariables["${STEP_VARIABLE_PREFIX}${index + 1}"] = result.result ?: 0.0 + + } catch (e: Exception) { + throw CalculatorException.stepExecutionError(index + 1, e) + } + } + + return results + } + + /** + * ์ˆ˜์‹์„ ํ† ํฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ํ† ํฐํ™”ํ•  ์ˆ˜์‹ + * @return ํ† ํฐ ๋ฆฌ์ŠคํŠธ + * @throws CalculatorException ํ† ํฐํ™” ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + private fun tokenize(formula: String): List { + val result = lexer.tokenize(formula) + return if (result.isSuccess) result.tokens else emptyList() + } + + /** + * ํ† ํฐ๋“ค์„ ํŒŒ์‹ฑํ•˜์—ฌ AST๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ํŒŒ์‹ฑํ•  ํ† ํฐ๋“ค + * @return AST ๋…ธ๋“œ + * @throws CalculatorException ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + private fun parseTokens(tokens: List): ASTNode { + return try { + parser.parse(tokens) + } catch (e: Exception) { + throw CalculatorException.stepExecutionError(2, e) + } + } + + /** + * ๊ณ„์‚ฐ ์š”์ฒญ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ฒ€์ฆํ•  ์š”์ฒญ + * @throws CalculatorException ์œ ํšจํ•˜์ง€ ์•Š์€ ์š”์ฒญ์ธ ๊ฒฝ์šฐ + */ + private fun validateRequest(request: CalculationRequest) { + if (request.formula.isBlank()) { + throw CalculatorException.emptyFormula() + } + + if (request.formula.length > maxFormulaLength) { + throw CalculatorException.formulaTooLong(request.formula, maxFormulaLength) + } + + if (request.variables.size > maxVariables) { + throw CalculatorException.tooManyVariables(request.variables.size, maxVariables) + } + } + + /** + * ๊ณ„์‚ฐ๊ธฐ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxFormulaLength" to maxFormulaLength, + "maxVariables" to maxVariables, + "lexerConfiguration" to lexer.getConfiguration(), + "supportedTokenTypes" to ALL_CALCULATOR_TOKENS, + "grammarStatistics" to Grammar.getGrammarStatistics() + ) + + /** + * ์ˆ˜์‹์˜ ๋ฌธ๋ฒ• ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ๊ฒ€์‚ฌํ•  ์ˆ˜์‹ + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + * @throws CalculatorException ๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun isValidFormula(formula: String): Boolean = try { + tokenize(formula) + true + } catch (e: Exception) { + throw CalculatorException.formulaValidationError(formula, e) + } + + /** + * ์ˆ˜์‹์—์„œ ์‚ฌ์šฉ๋œ ๋ณ€์ˆ˜๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ๋ถ„์„ํ•  ์ˆ˜์‹ + * @return ๋ณ€์ˆ˜ ์ด๋ฆ„ ์ง‘ํ•ฉ + * @throws CalculatorException ๋ณ€์ˆ˜ ์ถ”์ถœ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun extractVariables(formula: String): Set = try { + val tokens = tokenize(formula) + tokens.filter { it.type == TokenType.VARIABLE || it.type == TokenType.IDENTIFIER } + .map { it.value } + .toSet() + } catch (e: Exception) { + throw CalculatorException.variableExtractionError(formula, e) + } + + /** + * ๊ณ„์‚ฐ๊ธฐ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด + */ + fun getStatistics(): Map = mapOf( + "configuration" to getConfiguration(), + "lexerStats" to lexer.getStatistics(), + "grammarStats" to Grammar.getGrammarStatistics() + ) + + companion object { + + private const val ALL_CALCULATOR_TOKENS = "ALL_CALCULATOR_TOKENS" + private const val STEP_VARIABLE_PREFIX = "__entry_calc_step_" + /** + * ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return Calculator ์ธ์Šคํ„ด์Šค + */ + fun createDefault(): Calculator = Calculator() + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ์„ค์ •์œผ๋กœ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxFormulaLength ์ตœ๋Œ€ ์ˆ˜์‹ ๊ธธ์ด + * @param maxVariables ์ตœ๋Œ€ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @return Calculator ์ธ์Šคํ„ด์Šค + */ + fun create(maxFormulaLength: Int = 5000, maxVariables: Int = 100): Calculator = + Calculator(maxFormulaLength = maxFormulaLength, maxVariables = maxVariables) + + /** + * ๊ธฐ๋ณธ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ๊ณ„์‚ฐ๊ธฐ ์ธ์Šคํ„ด์Šค + */ + fun createBasic(): Calculator = Calculator( + maxFormulaLength = 1000, + maxVariables = 10 + ) + + /** + * ๊ณผํ•™ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณผํ•™ ๊ณ„์‚ฐ๊ธฐ ์ธ์Šคํ„ด์Šค + */ + fun createScientific(): Calculator = Calculator( + maxFormulaLength = 5000, + maxVariables = 100 + ) + + /** + * ํ†ต๊ณ„ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ๊ณ„์‚ฐ๊ธฐ ์ธ์Šคํ„ด์Šค + */ + fun createStatistical(): Calculator = Calculator( + maxFormulaLength = 10000, + maxVariables = 500 + ) + + /** + * ๊ณตํ•™ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณตํ•™ ๊ณ„์‚ฐ๊ธฐ ์ธ์Šคํ„ด์Šค + */ + fun createEngineering(): Calculator = Calculator( + maxFormulaLength = 15000, + maxVariables = 1000 + ) + + /** + * ์„ค์ •๊ณผ ํ•จ๊ป˜ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param settings ๊ณ„์‚ฐ๊ธฐ ์„ค์ • + * @return ์„ค์ •๋œ ๊ณ„์‚ฐ๊ธฐ ์ธ์Šคํ„ด์Šค + */ + fun createWithSettings(settings: Map): Calculator { + val maxFormula = (settings["maxFormulaLength"] as? Int) ?: 5000 + val maxVars = (settings["maxVariables"] as? Int) ?: 100 + return Calculator(maxFormulaLength = maxFormula, maxVariables = maxVars) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/entities/CalculationSession.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/entities/CalculationSession.kt new file mode 100644 index 00000000..2bf05f3b --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/entities/CalculationSession.kt @@ -0,0 +1,394 @@ +package hs.kr.entrydsm.domain.calculator.entities + +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import hs.kr.entrydsm.domain.calculator.values.CalculationResult +import hs.kr.entrydsm.global.annotation.entities.Entity +import java.time.Instant +import java.util.UUID +import java.util.concurrent.atomic.AtomicLong + +/** + * ๊ณ„์‚ฐ ์„ธ์…˜์„ ๊ด€๋ฆฌํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. + * + * DDD Entity ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž์˜ ๊ณ„์‚ฐ ์„ธ์…˜ ์ƒํƒœ์™€ ์ด๋ ฅ์„ + * ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ์—ฐ์†๋œ ๊ณ„์‚ฐ๋“ค์„ ๊ด€๋ฆฌํ•˜๊ณ  ๊ณ„์‚ฐ ๊ฒฐ๊ณผ์˜ ์žฌ์‚ฌ์šฉ๊ณผ + * ์ถ”์ ์„ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. + * + * @property sessionId ์„ธ์…˜ ๊ณ ์œ  ์‹๋ณ„์ž + * @property userId ์‚ฌ์šฉ์ž ์‹๋ณ„์ž + * @property createdAt ์ƒ์„ฑ ์‹œ๊ฐ„ + * @property lastActivity ๋งˆ์ง€๋ง‰ ํ™œ๋™ ์‹œ๊ฐ„ + * @property calculations ๊ณ„์‚ฐ ์ด๋ ฅ + * @property variables ์„ธ์…˜ ๋ณ€์ˆ˜๋“ค + * @property settings ์„ธ์…˜ ์„ค์ • + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Entity( + aggregateRoot = hs.kr.entrydsm.domain.calculator.aggregates.Calculator::class, + context = "calculator" +) +data class CalculationSession( + val sessionId: String, + val userId: String?, + val createdAt: Instant = Instant.now(), + val lastActivity: Instant = Instant.now(), + val calculations: List = emptyList(), + val variables: Map = emptyMap(), + val settings: CalculationSettings = CalculationSettings.default(), + val metadata: Map = emptyMap() +) { + + /** + * ๊ณ„์‚ฐ ์„ค์ •์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class CalculationSettings( + val precision: Int = 10, + val angleUnit: AngleUnit = AngleUnit.RADIANS, + val numberFormat: NumberFormat = NumberFormat.AUTO, + val enableCaching: Boolean = true, + val maxHistorySize: Int = 100, + val enableOptimization: Boolean = true, + val strictMode: Boolean = false + ) { + enum class AngleUnit { RADIANS, DEGREES } + enum class NumberFormat { AUTO, DECIMAL, SCIENTIFIC, ENGINEERING } + + companion object { + fun default() = CalculationSettings() + } + } + + init { + if (sessionId.isBlank()) { + throw CalculatorException.sessionIdEmpty(sessionId) + } + + if (calculations.size > settings.maxHistorySize) { + throw CalculatorException.calculationHistoryTooLarge( + calculations.size, + settings.maxHistorySize + ) + } + } + + /** + * ์ƒˆ๋กœ์šด ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ์ถ”๊ฐ€ํ•  ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + * @return ์—…๋ฐ์ดํŠธ๋œ ์„ธ์…˜ + */ + fun addCalculation(result: CalculationResult): CalculationSession { + val newCalculations = if (calculations.size >= settings.maxHistorySize) { + calculations.drop(1) + result + } else { + calculations + result + } + + return copy( + calculations = newCalculations, + lastActivity = Instant.now() + ) + } + + /** + * ๋ณ€์ˆ˜๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @param value ๋ณ€์ˆ˜ ๊ฐ’ + * @return ์—…๋ฐ์ดํŠธ๋œ ์„ธ์…˜ + */ + fun setVariable(name: String, value: Any): CalculationSession { + if (name.isBlank()) { + throw CalculatorException.variableNameEmpty(name) + } + + return copy( + variables = variables + (name to value), + lastActivity = Instant.now() + ) + } + + /** + * ์—ฌ๋Ÿฌ ๋ณ€์ˆ˜๋ฅผ ์ผ๊ด„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newVariables ์„ค์ •ํ•  ๋ณ€์ˆ˜๋“ค + * @return ์—…๋ฐ์ดํŠธ๋œ ์„ธ์…˜ + */ + fun setVariables(newVariables: Map): CalculationSession { + return copy( + variables = variables + newVariables, + lastActivity = Instant.now() + ) + } + + /** + * ๋ณ€์ˆ˜๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์ œ๊ฑฐํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ์—…๋ฐ์ดํŠธ๋œ ์„ธ์…˜ + */ + fun removeVariable(name: String): CalculationSession { + return copy( + variables = variables - name, + lastActivity = Instant.now() + ) + } + + /** + * ๋ณ€์ˆ˜ ๊ฐ’์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ๋ณ€์ˆ˜ ๊ฐ’ (์—†์œผ๋ฉด null) + */ + fun getVariable(name: String): Any? = variables[name] + + /** + * ๋ณ€์ˆ˜๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ์กด์žฌํ•˜๋ฉด true + */ + fun hasVariable(name: String): Boolean = name in variables + + /** + * ๋งˆ์ง€๋ง‰ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋งˆ์ง€๋ง‰ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ (์—†์œผ๋ฉด null) + */ + fun getLastResult(): CalculationResult? = calculations.lastOrNull() + + /** + * ํŠน์ • ์ธ๋ฑ์Šค์˜ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ์ธ๋ฑ์Šค (0๋ถ€ํ„ฐ ์‹œ์ž‘) + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ (์ธ๋ฑ์Šค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฉด null) + */ + fun getCalculation(index: Int): CalculationResult? { + return if (index in calculations.indices) calculations[index] else null + } + + /** + * ์„ฑ๊ณตํ•œ ๊ณ„์‚ฐ๋“ค๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณตํ•œ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋“ค + */ + fun getSuccessfulCalculations(): List { + return calculations.filter { it.isSuccess() } + } + + /** + * ์‹คํŒจํ•œ ๊ณ„์‚ฐ๋“ค๋งŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹คํŒจํ•œ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋“ค + */ + fun getFailedCalculations(): List { + return calculations.filter { !it.isSuccess() } + } + + /** + * ๊ณ„์‚ฐ ์ด๋ ฅ์„ ํด๋ฆฌ์–ดํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ด๋ ฅ์ด ํด๋ฆฌ์–ด๋œ ์„ธ์…˜ + */ + fun clearHistory(): CalculationSession { + return copy( + calculations = emptyList(), + lastActivity = Instant.now() + ) + } + + /** + * ๋ณ€์ˆ˜๋“ค์„ ํด๋ฆฌ์–ดํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€์ˆ˜๊ฐ€ ํด๋ฆฌ์–ด๋œ ์„ธ์…˜ + */ + fun clearVariables(): CalculationSession { + return copy( + variables = emptyMap(), + lastActivity = Instant.now() + ) + } + + /** + * ์„ธ์…˜์„ ์™„์ „ํžˆ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ดˆ๊ธฐํ™”๋œ ์„ธ์…˜ + */ + fun reset(): CalculationSession { + return copy( + calculations = emptyList(), + variables = emptyMap(), + lastActivity = Instant.now() + ) + } + + /** + * ์„ธ์…˜ ์„ค์ •์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. + * + * @param newSettings ์ƒˆ๋กœ์šด ์„ค์ • + * @return ์—…๋ฐ์ดํŠธ๋œ ์„ธ์…˜ + */ + fun updateSettings(newSettings: CalculationSettings): CalculationSession { + val adjustedCalculations = if (newSettings.maxHistorySize < calculations.size) { + calculations.takeLast(newSettings.maxHistorySize) + } else { + calculations + } + + return copy( + settings = newSettings, + calculations = adjustedCalculations, + lastActivity = Instant.now() + ) + } + + /** + * ์„ธ์…˜์ด ํ™œ์„ฑ ์ƒํƒœ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param timeoutMinutes ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„ (๋ถ„) + * @return ํ™œ์„ฑ ์ƒํƒœ์ด๋ฉด true + */ + fun isActive(timeoutMinutes: Long = 30): Boolean { + val now = Instant.now() + val timeoutInstant = lastActivity.plusSeconds(timeoutMinutes * 60) + return now.isBefore(timeoutInstant) + } + + /** + * ์„ธ์…˜์˜ ์ด ๊ณ„์‚ฐ ์‹œ๊ฐ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ด ๊ณ„์‚ฐ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun getTotalCalculationTime(): Long { + return calculations.sumOf { it.executionTimeMs } + } + + /** + * ํ‰๊ท  ๊ณ„์‚ฐ ์‹œ๊ฐ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ‰๊ท  ๊ณ„์‚ฐ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun getAverageCalculationTime(): Double { + return if (calculations.isEmpty()) 0.0 else getTotalCalculationTime().toDouble() / calculations.size + } + + /** + * ์„ฑ๊ณต๋ฅ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณต๋ฅ  (0.0 ~ 1.0) + */ + fun getSuccessRate(): Double { + return if (calculations.isEmpty()) 0.0 else getSuccessfulCalculations().size.toDouble() / calculations.size + } + + /** + * ์„ธ์…˜์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "sessionId" to sessionId, + "userId" to (userId ?: "anonymous"), + "totalCalculations" to calculations.size, + "successfulCalculations" to getSuccessfulCalculations().size, + "failedCalculations" to getFailedCalculations().size, + "successRate" to getSuccessRate(), + "totalVariables" to variables.size, + "totalCalculationTime" to getTotalCalculationTime(), + "averageCalculationTime" to getAverageCalculationTime(), + "sessionDuration" to (lastActivity.epochSecond - createdAt.epochSecond), + "isActive" to isActive(), + "settings" to settings + ) + + /** + * ์„ธ์…˜์„ ์š”์•ฝํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ธ์…˜ ์š”์•ฝ ๋ฌธ์ž์—ด + */ + fun summarize(): String = buildString { + appendLine("=== ๊ณ„์‚ฐ ์„ธ์…˜ ์š”์•ฝ ===") + appendLine("์„ธ์…˜ ID: $sessionId") + appendLine("์‚ฌ์šฉ์ž: ${userId ?: "์ต๋ช…"}") + appendLine("์ด ๊ณ„์‚ฐ: ${calculations.size}๊ฐœ") + appendLine("์„ฑ๊ณต๋ฅ : ${"%.1f".format(getSuccessRate() * 100)}%") + appendLine("๋ณ€์ˆ˜: ${variables.size}๊ฐœ") + appendLine("์ด ๊ณ„์‚ฐ ์‹œ๊ฐ„: ${getTotalCalculationTime()}ms") + appendLine("ํ‰๊ท  ๊ณ„์‚ฐ ์‹œ๊ฐ„: ${"%.2f".format(getAverageCalculationTime())}ms") + appendLine("์„ธ์…˜ ์ง€์† ์‹œ๊ฐ„: ${(lastActivity.epochSecond - createdAt.epochSecond)}์ดˆ") + appendLine("ํ™œ์„ฑ ์ƒํƒœ: ${if (isActive()) "์˜ˆ" else "์•„๋‹ˆ์˜ค"}") + } + + companion object { + /** + * ๋™์‹œ์„ฑ ํ™˜๊ฒฝ์—์„œ ๊ณ ์œ ํ•œ ์„ธ์…˜ ID ์ƒ์„ฑ์„ ์œ„ํ•œ atomic counter + */ + private val sessionCounter = AtomicLong(0) + + /** + * ๋™์‹œ์„ฑ ํ™˜๊ฒฝ์—์„œ ๊ณ ์œ ํ•œ ์„ธ์…˜ ID๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ํƒ€์ž„์Šคํƒฌํ”„, atomic counter, UUID๋ฅผ ์กฐํ•ฉํ•˜์—ฌ ์ถฉ๋Œ ๊ฐ€๋Šฅ์„ฑ์„ ์ตœ์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param prefix ์„ธ์…˜ ID ์ ‘๋‘์‚ฌ + * @param includeUuid UUID ํฌํ•จ ์—ฌ๋ถ€ (๊ธฐ๋ณธ๊ฐ’: true) + * @return ๊ณ ์œ ํ•œ ์„ธ์…˜ ID + */ + private fun generateUniqueSessionId(prefix: String, includeUuid: Boolean = true): String { + val timestamp = System.currentTimeMillis() + val counter = sessionCounter.incrementAndGet() + + return if (includeUuid) { + val uuid = UUID.randomUUID().toString().take(8) + "${prefix}_${timestamp}_${counter}_${uuid}" + } else { + "${prefix}_${timestamp}_${counter}" + } + } + + /** + * ์ƒˆ๋กœ์šด ์„ธ์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param sessionId ์„ธ์…˜ ID + * @param userId ์‚ฌ์šฉ์ž ID + * @return ์ƒˆ๋กœ์šด ์„ธ์…˜ + */ + fun create(sessionId: String, userId: String? = null): CalculationSession { + if (sessionId.isBlank()) { + throw CalculatorException.sessionIdEmpty(sessionId) + } + + return CalculationSession(sessionId = sessionId, userId = userId) + } + + /** + * ์ž„์‹œ ์„ธ์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๋™์‹œ์„ฑ ํ™˜๊ฒฝ์—์„œ ๊ณ ์œ ์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด ํƒ€์ž„์Šคํƒฌํ”„, UUID, atomic counter๋ฅผ ์กฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž„์‹œ ์„ธ์…˜ + */ + fun createTemporary(): CalculationSession { + val sessionId = generateUniqueSessionId("temp", includeUuid = true) + return create(sessionId) + } + + /** + * ์‚ฌ์šฉ์ž ์„ธ์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๋™์‹œ์„ฑ ํ™˜๊ฒฝ์—์„œ ๊ณ ์œ ์„ฑ์„ ๋ณด์žฅํ•˜๊ธฐ ์œ„ํ•ด ํƒ€์ž„์Šคํƒฌํ”„, atomic counter๋ฅผ ์กฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ์‚ฌ์šฉ์ž ์„ธ์…˜ + */ + fun createForUser(userId: String): CalculationSession { + if (userId.isBlank()) { + throw CalculatorException.userIdEmpty(userId) + } + + val sessionId = generateUniqueSessionId("user_${userId}", includeUuid = false) + return create(sessionId, userId) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/exceptions/CalculatorException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/exceptions/CalculatorException.kt new file mode 100644 index 00000000..651f1b8a --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/exceptions/CalculatorException.kt @@ -0,0 +1,548 @@ +package hs.kr.entrydsm.domain.calculator.exceptions + +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * Calculator ๋„๋ฉ”์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ์ˆ˜์‹ ์ฒ˜๋ฆฌ, ๊ณ„์‚ฐ ๋‹จ๊ณ„ ์‹คํ–‰, ๋ณ€์ˆ˜ ๊ด€๋ฆฌ ๋“ฑ์˜ ๊ณ„์‚ฐ๊ธฐ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์—์„œ + * ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @property formula ์˜ค๋ฅ˜์™€ ๊ด€๋ จ๋œ ์ˆ˜์‹ (์„ ํƒ์‚ฌํ•ญ) + * @property step ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ณ„์‚ฐ ๋‹จ๊ณ„ (์„ ํƒ์‚ฌํ•ญ) + * @property variableCount ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ (์„ ํƒ์‚ฌํ•ญ) + * @property maxAllowed ํ—ˆ์šฉ๋œ ์ตœ๋Œ€๊ฐ’ (์„ ํƒ์‚ฌํ•ญ) + * @property missingVariables ๋ˆ„๋ฝ๋œ ๋ณ€์ˆ˜ ๋ฆฌ์ŠคํŠธ (์„ ํƒ์‚ฌํ•ญ) + * @property reason ์‚ฌ์œ  (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +class CalculatorException( + errorCode: ErrorCode, + val formula: String? = null, + val step: Int? = null, + val variableCount: Int? = null, + val maxAllowed: Int? = null, + val missingVariables: List = emptyList(), + val reason: String? = null, + message: String = buildCalculatorMessage(errorCode, formula, step, variableCount, maxAllowed, missingVariables, reason), + cause: Throwable? = null +) : DomainException(errorCode, message, cause) { + + companion object { + /** + * Calculator ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param errorCode ์˜ค๋ฅ˜ ์ฝ”๋“œ + * @param formula ์ˆ˜์‹ + * @param step ๊ณ„์‚ฐ ๋‹จ๊ณ„ + * @param variableCount ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @param maxAllowed ์ตœ๋Œ€ ํ—ˆ์šฉ๊ฐ’ + * @param missingVariables ๋ˆ„๋ฝ๋œ ๋ณ€์ˆ˜๋“ค + * @param reason ์‚ฌ์œ  + * @return ๊ตฌ์„ฑ๋œ ๋ฉ”์‹œ์ง€ + */ + private fun buildCalculatorMessage( + errorCode: ErrorCode, + formula: String?, + step: Int?, + variableCount: Int?, + maxAllowed: Int?, + missingVariables: List, + reason: String? + ): String { + val baseMessage = errorCode.description + val details = mutableListOf() + + formula?.let { details.add("์ˆ˜์‹: $it") } + step?.let { details.add("๋‹จ๊ณ„: $it") } + reason?.let { details.add("์‚ฌ์œ : $it") } + + if (variableCount != null && maxAllowed != null) { + details.add("๋ณ€์ˆ˜: $variableCount (์ตœ๋Œ€: $maxAllowed)") + } else { + variableCount?.let { details.add("๋ณ€์ˆ˜๊ฐœ์ˆ˜: $it") } + maxAllowed?.let { details.add("์ตœ๋Œ€ํ—ˆ์šฉ: $it") } + } + if (missingVariables.isNotEmpty()) { + details.add("๋ˆ„๋ฝ๋ณ€์ˆ˜: ${missingVariables.joinToString(", ")}") + } + + return if (details.isNotEmpty()) { + "$baseMessage (${details.joinToString(", ")})" + } else { + baseMessage + } + } + + /** + * ๋นˆ ์ˆ˜์‹ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun emptyFormula(): CalculatorException { + return CalculatorException( + errorCode = ErrorCode.EMPTY_FORMULA + ) + } + + /** + * ์ˆ˜์‹์ด ๋„ˆ๋ฌด ๊ธด ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ๋„ˆ๋ฌด ๊ธด ์ˆ˜์‹ + * @param maxLength ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ธธ์ด + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun formulaTooLong(formula: String, maxLength: Int): CalculatorException { + return CalculatorException( + errorCode = ErrorCode.FORMULA_TOO_LONG, + formula = formula.take(50) + if (formula.length > 50) "..." else "", + maxAllowed = maxLength + ) + } + + /** + * ๋นˆ ๊ณ„์‚ฐ ๋‹จ๊ณ„ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun emptySteps(): CalculatorException { + return CalculatorException( + errorCode = ErrorCode.EMPTY_STEPS + ) + } + + /** + * ๊ณ„์‚ฐ ๋‹จ๊ณ„๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param stepCount ์‹ค์ œ ๋‹จ๊ณ„ ์ˆ˜ + * @param maxSteps ์ตœ๋Œ€ ํ—ˆ์šฉ ๋‹จ๊ณ„ ์ˆ˜ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun tooManySteps(stepCount: Int, maxSteps: Int): CalculatorException { + return CalculatorException( + errorCode = ErrorCode.TOO_MANY_STEPS, + variableCount = stepCount, + maxAllowed = maxSteps + ) + } + + /** + * ๋ณ€์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param variableCount ์‹ค์ œ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @param maxVariables ์ตœ๋Œ€ ํ—ˆ์šฉ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun tooManyVariables(variableCount: Int, maxVariables: Int): CalculatorException { + return CalculatorException( + errorCode = ErrorCode.TOO_MANY_VARIABLES, + variableCount = variableCount, + maxAllowed = maxVariables + ) + } + + /** + * ํ•„์ˆ˜ ๋ณ€์ˆ˜ ๋ˆ„๋ฝ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param missingVariables ๋ˆ„๋ฝ๋œ ๋ณ€์ˆ˜๋“ค + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun missingVariables(missingVariables: List): CalculatorException { + return CalculatorException( + errorCode = ErrorCode.MISSING_VARIABLES, + missingVariables = missingVariables + ) + } + + /** + * ๋‹จ๊ณ„ ์‹คํ–‰ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param step ์‹คํ–‰ ์‹คํŒจํ•œ ๋‹จ๊ณ„ + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun stepExecutionError(step: Int, cause: Throwable? = null): CalculatorException { + return CalculatorException( + errorCode = ErrorCode.STEP_EXECUTION_ERROR, + step = step, + cause = cause + ) + } + + /** + * ์ˆ˜์‹ ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ๊ฒ€์ฆ ์‹คํŒจํ•œ ์ˆ˜์‹ + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun formulaValidationError(formula: String, cause: Throwable? = null): CalculatorException { + return CalculatorException( + errorCode = ErrorCode.FORMULA_VALIDATION_ERROR, + formula = formula, + cause = cause + ) + } + + /** + * ๋ณ€์ˆ˜ ์ถ”์ถœ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ๋ณ€์ˆ˜ ์ถ”์ถœ ์‹คํŒจํ•œ ์ˆ˜์‹ + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun variableExtractionError(formula: String, cause: Throwable? = null): CalculatorException { + return CalculatorException( + errorCode = ErrorCode.VARIABLE_EXTRACTION_ERROR, + formula = formula, + cause = cause + ) + } + + /** + * ์„ธ์…˜ ID๊ฐ€ ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ์„ธ์…˜ ID + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun sessionIdEmpty(actual: String?): CalculatorException = + CalculatorException( + errorCode = ErrorCode.SESSION_ID_EMPTY, + reason = "actual=${actual ?: "null"}" + ) + + /** + * ๊ณ„์‚ฐ ์ด๋ ฅ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํฌ๊ธฐ๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ํ˜„์žฌ ๊ณ„์‚ฐ ์ด๋ ฅ ํฌ๊ธฐ + * @param max ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ํฌ๊ธฐ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun calculationHistoryTooLarge(actual: Int, max: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.CALCULATION_HISTORY_TOO_LARGE, + reason = "actual=$actual, max=$max" + ) + + /** + * ๋ณ€์ˆ˜ ์ด๋ฆ„์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ๋ณ€์ˆ˜๋ช… + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun variableNameEmpty(actual: String?): CalculatorException = + CalculatorException( + errorCode = ErrorCode.VARIABLE_NAME_EMPTY, + reason = "actual=${actual ?: "null"}" + ) + + /** + * ์‚ฌ์šฉ์ž ID๊ฐ€ ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ์‚ฌ์šฉ์ž ID + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun userIdEmpty(actual: String?): CalculatorException = + CalculatorException( + errorCode = ErrorCode.USER_ID_EMPTY, + reason = "actual=${actual ?: "null"}" + ) + + /** + * ์ˆ˜์‹ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun emptyExpressions(): CalculatorException = + CalculatorException( + errorCode = ErrorCode.EMPTY_EXPRESSIONS + ) + + /** + * ๊ณ„์‚ฐ ๋‹จ๊ณ„๊ฐ€ ์ „์ฒด ํƒ€์ž„์•„์›ƒ์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param stepIndex ์ดˆ๊ณผํ•œ ๋‹จ๊ณ„ ์ธ๋ฑ์Šค + * @param remainingTime ๋‚จ์€ ์‹œ๊ฐ„(ms) + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun stepTimeoutExceeded(stepIndex: Int, remainingTime: Long): CalculatorException = + CalculatorException( + errorCode = ErrorCode.STEP_TIMEOUT_EXCEEDED, + reason = "stepIndex=$stepIndex, remainingTime=$remainingTime" + ) + + /** + * ๊ณ„์‚ฐ ์š”์ฒญ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun requestListEmpty(): CalculatorException = + CalculatorException( + errorCode = ErrorCode.REQUEST_LIST_EMPTY + ) + + /** + * ๋™์‹œ์„ฑ ์ˆ˜์ค€์ด 0 ์ดํ•˜์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ๋™์‹œ์„ฑ ์ˆ˜์ค€ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun invalidConcurrencyLevel(actual: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.INVALID_CONCURRENCY_LEVEL, + reason = "actual=$actual" + ) + + /** + * ๋ฒ„ํผ ํฌ๊ธฐ๊ฐ€ 0 ์ดํ•˜์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ๋ฒ„ํผ ํฌ๊ธฐ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun invalidBufferSize(actual: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.INVALID_BUFFER_SIZE, + reason = "actual=$actual" + ) + + /** + * ์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun maxRetryExceeded(): CalculatorException = + CalculatorException( + errorCode = ErrorCode.MAX_RETRY_EXCEEDED + ) + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ AST ๋…ธ๋“œ ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์ž…๋ ฅ๋œ AST ๋…ธ๋“œ ํƒ€์ž… + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun invalidAstNodeType(actualType: String): CalculatorException = + CalculatorException( + errorCode = ErrorCode.INVALID_AST_NODE_TYPE, + reason = "actualType=$actualType" + ) + + /** + * ์˜ต์…˜ ํ‚ค๊ฐ€ ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ์˜ต์…˜ ํ‚ค + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun optionKeyEmpty(actual: String?): CalculatorException = + CalculatorException( + errorCode = ErrorCode.OPTION_KEY_EMPTY, + reason = "actual=${actual ?: "null"}" + ) + + /** + * ์‹คํ–‰ ์‹œ๊ฐ„์ด 0๋ณด๋‹ค ์ž‘์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ์‹คํ–‰ ์‹œ๊ฐ„(ms) + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun executionTimeNegative(actual: Long): CalculatorException = + CalculatorException( + errorCode = ErrorCode.EXECUTION_TIME_NEGATIVE, + reason = "actual=$actual" + ) + + /** + * ๋ณ‘ํ•ฉํ•  ๊ฒฐ๊ณผ๊ฐ€ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun mergeResultsEmpty(): CalculatorException = + CalculatorException( + errorCode = ErrorCode.MERGE_RESULTS_EMPTY + ) + + /** + * ๋‹จ๊ณ„ ์ด๋ฆ„์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ๋‹จ๊ณ„ ์ด๋ฆ„ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun stepNameEmpty(actual: String?): CalculatorException = + CalculatorException( + errorCode = ErrorCode.STEP_NAME_EMPTY, + reason = "actual=${actual ?: "null"}" + ) + + /** + * ๋‹จ๊ณ„ ์ด๋ฆ„์ด ๋„ˆ๋ฌด ๊ธธ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualLength ์‹ค์ œ ๊ธธ์ด + * @param maxLength ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ๊ธธ์ด + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun stepNameTooLong(actualLength: Int, maxLength: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.STEP_NAME_TOO_LONG, + reason = "length=$actualLength, max=$maxLength" + ) + + /** + * ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช…์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ๋ณ€์ˆ˜๋ช… + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun resultVariableNameEmpty(actual: String?): CalculatorException = + CalculatorException( + errorCode = ErrorCode.RESULT_VARIABLE_NAME_EMPTY, + reason = "actual=${actual ?: "null"}" + ) + + /** + * ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช…์ด ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ๋ณ€์ˆ˜๋ช… + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun resultVariableNameInvalid(actual: String): CalculatorException = + CalculatorException( + errorCode = ErrorCode.RESULT_VARIABLE_NAME_INVALID, + reason = "actual=$actual" + ) + + /** + * ๊ณ„์‚ฐ ๋‹จ๊ณ„ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun stepsEmpty(): CalculatorException = + CalculatorException(errorCode = ErrorCode.STEPS_EMPTY) + + /** + * ๊ณ„์‚ฐ ๋‹จ๊ณ„ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ์น˜๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๊ณ„์‚ฐ ๋‹จ๊ณ„ ๊ฐœ์ˆ˜ + * @param max ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ๊ณ„์‚ฐ ๋‹จ๊ณ„ ๊ฐœ์ˆ˜ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun stepsTooMany(actual: Int, max: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.STEPS_TOO_MANY, + reason = "actual=$actual, max=$max" + ) + + /** + * ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ์น˜๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @param max ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun variablesTooMany(actual: Int, max: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.VARIABLES_TOO_MANY, + reason = "actual=$actual, max=$max" + ) + + /** + * ํŠน์ • ๋‹จ๊ณ„์˜ ์ˆ˜์‹์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param stepIndex ์ˆ˜์‹์ด ๋น„์–ด ์žˆ๋Š” ๋‹จ๊ณ„์˜ ์ธ๋ฑ์Šค (0๋ถ€ํ„ฐ ์‹œ์ž‘) + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun stepFormulaEmpty(stepIndex: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.STEP_FORMULA_EMPTY, + reason = "stepIndex=$stepIndex" + ) + + /** + * ์ธ๋ฑ์Šค๊ฐ€ [0..maxInclusive] ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ „๋‹ฌ๋œ ์ธ๋ฑ์Šค ๊ฐ’ + * @param maxInclusive ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์ธ๋ฑ์Šค ๊ฐ’ (ํฌํ•จ) + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun indexOutOfRangeInclusive(actual: Int, maxInclusive: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.INDEX_OUT_OF_RANGE_INCLUSIVE, + reason = "index=$actual, range=0..$maxInclusive" + ) + + /** + * ์ธ๋ฑ์Šค๊ฐ€ [0..maxExclusive) ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ „๋‹ฌ๋œ ์ธ๋ฑ์Šค ๊ฐ’ + * @param maxExclusive ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์ธ๋ฑ์Šค ๊ฐ’ (๋ฏธํฌํ•จ) + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun indexOutOfRangeExclusive(actual: Int, maxExclusive: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.INDEX_OUT_OF_RANGE_EXCLUSIVE, + reason = "index=$actual, range=0..${maxExclusive - 1}" + ) + + /** + * ์ตœ์†Œ ๋‹จ๊ณ„ ๊ฐœ์ˆ˜ ์š”๊ฑด์„ ์ถฉ์กฑํ•˜์ง€ ๋ชปํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๋‹จ๊ณ„ ๊ฐœ์ˆ˜ + * @param min ์š”๊ตฌ๋˜๋Š” ์ตœ์†Œ ๋‹จ๊ณ„ ๊ฐœ์ˆ˜ + * @return CalculatorException ์ธ์Šคํ„ด์Šค + */ + fun minStepsRequired(actual: Int, min: Int): CalculatorException = + CalculatorException( + errorCode = ErrorCode.MIN_STEPS_REQUIRED, + reason = "actual=$actual, min=$min" + ) + } + + /** + * Calculator ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆ˜์‹, ๋‹จ๊ณ„, ๋ณ€์ˆ˜ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋งต + */ + fun getCalculatorInfo(): Map { + val info = mutableMapOf() + + formula?.let { info["formula"] = it } + step?.let { info["step"] = it } + variableCount?.let { info["variableCount"] = it } + maxAllowed?.let { info["maxAllowed"] = it } + if (missingVariables.isNotEmpty()) { info["missingVariables"] = missingVariables } + + return info + } + + /** + * ์ „์ฒด ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ์ •๋ณด์™€ Calculator ์ •๋ณด๊ฐ€ ๊ฒฐํ•ฉ๋œ ๋งต + */ + fun getFullErrorInfo(): Map { + val baseInfo = super.toErrorInfo().toMutableMap() + val calculatorInfo = getCalculatorInfo() + + calculatorInfo.forEach { (key, value) -> + when (value) { + is List<*> -> baseInfo[key] = value.joinToString(", ") + else -> baseInfo[key] = value?.toString() ?: "" + } + } + + return baseInfo + } + + override fun toString(): String { + val calculatorDetails = getCalculatorInfo() + return if (calculatorDetails.isNotEmpty()) { + "${super.toString()}, calculator=${calculatorDetails}" + } else { + super.toString() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/factories/CalculatorFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/factories/CalculatorFactory.kt new file mode 100644 index 00000000..c0ee908f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/factories/CalculatorFactory.kt @@ -0,0 +1,421 @@ +package hs.kr.entrydsm.domain.calculator.factories + +import hs.kr.entrydsm.domain.calculator.aggregates.Calculator +import hs.kr.entrydsm.domain.calculator.entities.CalculationSession +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import hs.kr.entrydsm.domain.calculator.values.CalculationResult +import hs.kr.entrydsm.domain.factories.EnvironmentFactory +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.annotation.specification.type.Priority +import java.util.concurrent.atomic.AtomicLong + +/** + * Calculator ๋„๋ฉ”์ธ ๊ฐ์ฒด๋“ค์„ ์ƒ์„ฑํ•˜๋Š” ํŒฉํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. + * + * DDD Factory ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๊ณ„์‚ฐ๊ธฐ์™€ ๊ด€๋ จ๋œ ๊ฐ์ฒด๋“ค์˜ ์ƒ์„ฑ๊ณผ ๊ตฌ์„ฑ์„ + * ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ์œ ํ˜•์˜ ๊ณ„์‚ฐ๊ธฐ์™€ ์„ธ์…˜์„ ์ƒ์„ฑํ•˜๊ณ  + * ์ ์ ˆํ•œ ์„ค์ •๊ณผ ์ •์ฑ…์„ ์ ์šฉํ•˜์—ฌ ์ผ๊ด€๋œ ๊ฐ์ฒด ์ƒ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Factory( + context = "calculator", + complexity = Complexity.HIGH, + cache = true +) +class CalculatorFactory { + + companion object { + private val createdCalculatorCount = AtomicLong(0L) + private val createdSessionCount = AtomicLong(0L) + private val createdRequestCount = AtomicLong(0L) + + @Volatile + private var instance: CalculatorFactory? = null + + fun getInstance(): CalculatorFactory { + return instance ?: synchronized(this) { + instance ?: CalculatorFactory().also { instance = it } + } + } + + // ํŽธ์˜ ๋ฉ”์„œ๋“œ๋“ค + fun quickCreateBasicCalculator(): Calculator = getInstance().createBasicCalculator() + fun quickCreateScientificCalculator(): Calculator = getInstance().createScientificCalculator() + fun quickCreateSession(userId: String? = null): CalculationSession = getInstance().createSession(userId) + fun quickCreateRequest(expression: String, variables: Map = emptyMap()): CalculationRequest = + getInstance().createRequest(expression, variables) + } + + /** + * ๊ธฐ๋ณธ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์„ค์ •์˜ ๊ณ„์‚ฐ๊ธฐ + */ + fun createBasicCalculator(): Calculator { + createdCalculatorCount.incrementAndGet() + return Calculator.createBasic() + } + + /** + * ๊ณผํ•™ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณผํ•™ ๊ณ„์‚ฐ ๊ธฐ๋Šฅ์ด ํฌํ•จ๋œ ๊ณ„์‚ฐ๊ธฐ + */ + fun createScientificCalculator(): Calculator { + createdCalculatorCount.incrementAndGet() + return Calculator.createScientific() + } + + /** + * ํ†ต๊ณ„ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ํ•จ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ๊ณ„์‚ฐ๊ธฐ + */ + fun createStatisticalCalculator(): Calculator { + createdCalculatorCount.incrementAndGet() + return Calculator.createStatistical() + } + + /** + * ๊ณตํ•™์šฉ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณตํ•™ ๊ณ„์‚ฐ ๊ธฐ๋Šฅ์ด ํฌํ•จ๋œ ๊ณ„์‚ฐ๊ธฐ + */ + fun createEngineeringCalculator(): Calculator { + createdCalculatorCount.incrementAndGet() + return Calculator.createEngineering() + } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ์„ค์ •์œผ๋กœ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param precision ์ •๋ฐ€๋„ + * @param angleUnit ๊ฐ๋„ ๋‹จ์œ„ + * @param enableCaching ์บ์‹ฑ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @param enableOptimization ์ตœ์ ํ™” ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @return ์‚ฌ์šฉ์ž ์ •์˜ ๊ณ„์‚ฐ๊ธฐ + */ + fun createCustomCalculator( + precision: Int = 10, + angleUnit: CalculationSession.CalculationSettings.AngleUnit = CalculationSession.CalculationSettings.AngleUnit.RADIANS, + enableCaching: Boolean = true, + enableOptimization: Boolean = true + ): Calculator { + createdCalculatorCount.incrementAndGet() + + val settingsMap = mapOf( + "precision" to precision, + "angleUnit" to angleUnit, + "enableCaching" to enableCaching, + "enableOptimization" to enableOptimization + ) + + return Calculator.createWithSettings(settingsMap) + } + + /** + * ์ƒˆ๋กœ์šด ๊ณ„์‚ฐ ์„ธ์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param userId ์‚ฌ์šฉ์ž ID (์„ ํƒ์ ) + * @return ์ƒˆ๋กœ์šด ๊ณ„์‚ฐ ์„ธ์…˜ + */ + fun createSession(userId: String? = null): CalculationSession { + createdSessionCount.incrementAndGet() + return if (userId != null) { + CalculationSession.createForUser(userId) + } else { + CalculationSession.createTemporary() + } + } + + /** + * ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ์‚ฌ์šฉ์ž ์„ธ์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param userId ์‚ฌ์šฉ์ž ID + * @return ์‚ฌ์šฉ์ž ์„ธ์…˜ + */ + fun createUserSession(userId: String): CalculationSession { + if (userId.isBlank()) { + throw CalculatorException.userIdEmpty(userId) + } + + createdSessionCount.incrementAndGet() + return CalculationSession.createForUser(userId) + } + + /** + * ์ž„์‹œ ์„ธ์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž„์‹œ ์„ธ์…˜ + */ + fun createTemporarySession(): CalculationSession { + createdSessionCount.incrementAndGet() + return CalculationSession.createTemporary() + } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ์„ค์ •์œผ๋กœ ์„ธ์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param sessionId ์„ธ์…˜ ID + * @param userId ์‚ฌ์šฉ์ž ID + * @param settings ๊ณ„์‚ฐ ์„ค์ • + * @param variables ์ดˆ๊ธฐ ๋ณ€์ˆ˜๋“ค + * @return ์‚ฌ์šฉ์ž ์ •์˜ ์„ธ์…˜ + */ + fun createCustomSession( + sessionId: String, + userId: String? = null, + settings: CalculationSession.CalculationSettings = CalculationSession.CalculationSettings.default(), + variables: Map = emptyMap() + ): CalculationSession { + createdSessionCount.incrementAndGet() + return CalculationSession( + sessionId = sessionId, + userId = userId, + variables = variables, + settings = settings + ) + } + + /** + * ๊ณ„์‚ฐ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ + * @param variables ๋ณ€์ˆ˜๋“ค (์„ ํƒ์ ) + * @return ๊ณ„์‚ฐ ์š”์ฒญ + */ + fun createRequest( + formula: String, + variables: Map = emptyMap() + ): CalculationRequest { + if (formula.isBlank()) { + throw CalculatorException.emptyFormula() + } + createdRequestCount.incrementAndGet() + + return CalculationRequest( + formula = formula, + variables = variables + ) + } + + /** + * ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์žˆ๋Š” ๊ณ„์‚ฐ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ + * @param priority ์šฐ์„ ์ˆœ์œ„ + * @param variables ๋ณ€์ˆ˜๋“ค + * @return ์šฐ์„ ์ˆœ์œ„ ๊ณ„์‚ฐ ์š”์ฒญ + */ + fun createPriorityRequest( + formula: String, + priority: Priority, + variables: Map = emptyMap() + ): CalculationRequest { + createdRequestCount.incrementAndGet() + val options = mapOf("priority" to priority.name) + return CalculationRequest( + formula = formula, + variables = variables, + options = options + ) + } + + /** + * ์ผ๊ด„ ๊ณ„์‚ฐ ์š”์ฒญ๋“ค์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expressions ์ˆ˜์‹๋“ค + * @param variables ๊ณตํ†ต ๋ณ€์ˆ˜๋“ค + * @return ๊ณ„์‚ฐ ์š”์ฒญ ๋ชฉ๋ก + */ + fun createBatchRequests( + expressions: List, + variables: Map = emptyMap() + ): List { + if (expressions.isEmpty()) { + throw CalculatorException.emptyExpressions() + } + + return expressions.map { expression -> + createRequest(expression, variables) + } + } + + /** + * ์„ฑ๊ณตํ•œ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param requestId ์š”์ฒญ ID + * @param result ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ๊ฐ’ + * @param executionTime ์‹คํ–‰ ์‹œ๊ฐ„ + * @return ์„ฑ๊ณต ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + */ + fun createSuccessResult( + formula: String, + result: Any, + executionTimeMs: Long = 0 + ): CalculationResult { + return CalculationResult( + result = result, + executionTimeMs = executionTimeMs, + formula = formula + ) + } + + /** + * ์‹คํŒจํ•œ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param requestId ์š”์ฒญ ID + * @param error ์˜ค๋ฅ˜ ์ •๋ณด + * @param executionTime ์‹คํ–‰ ์‹œ๊ฐ„ + * @return ์‹คํŒจ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + */ + fun createFailureResult( + formula: String, + error: String, + executionTimeMs: Long = 0 + ): CalculationResult { + return CalculationResult( + result = null, + executionTimeMs = executionTimeMs, + formula = formula, + errors = listOf(error) + ) + } + + /** + * ์˜ˆ์™ธ๋กœ๋ถ€ํ„ฐ ์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param requestId ์š”์ฒญ ID + * @param exception ์˜ˆ์™ธ + * @param executionTime ์‹คํ–‰ ์‹œ๊ฐ„ + * @return ์‹คํŒจ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + */ + fun createFailureFromException( + formula: String, + exception: Exception, + executionTimeMs: Long = 0 + ): CalculationResult { + return CalculationResult( + result = null, + executionTimeMs = executionTimeMs, + formula = formula, + errors = listOf("๊ณ„์‚ฐ ์˜ค๋ฅ˜: ${exception.message}") + ) + } + + /** + * ๊ธฐ๋ณธ ๋ณ€์ˆ˜๋“ค์„ ๊ฐ€์ง„ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ๋ณ€์ˆ˜ ๋งต + */ + fun createDefaultEnvironment(): Map { + return EnvironmentFactory.createBasicEnvironment().mapValues { it.value } + } + + /** + * ๊ณผํ•™ ๊ณ„์‚ฐ์šฉ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณผํ•™ ์ƒ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ๋ณ€์ˆ˜ ๋งต + */ + fun createScientificEnvironment(): Map { + return EnvironmentFactory.createScientificEnvironment().mapValues { it.value } + } + + /** + * ๊ณตํ•™์šฉ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณตํ•™ ์ƒ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ๋ณ€์ˆ˜ ๋งต + */ + fun createEngineeringEnvironment(): Map { + return EnvironmentFactory.createEngineeringEnvironment().mapValues { it.value } + } + + /** + * ํ†ต๊ณ„์šฉ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ƒ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ๋ณ€์ˆ˜ ๋งต + */ + fun createStatisticalEnvironment(): Map { + return EnvironmentFactory.createStatisticalEnvironment().mapValues { it.value } + } + + /** + * ๊ณ ์„ฑ๋Šฅ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxConcurrency ์ตœ๋Œ€ ๋™์‹œ ๊ณ„์‚ฐ ์ˆ˜ + * @param cacheSize ์บ์‹œ ํฌ๊ธฐ + * @return ๊ณ ์„ฑ๋Šฅ ๊ณ„์‚ฐ๊ธฐ + */ + fun createHighPerformanceCalculator( + maxConcurrency: Int = 10, + cacheSize: Int = 1000 + ): Calculator { + createdCalculatorCount.incrementAndGet() + + val settingsMap = mapOf( + "precision" to 15, + "enableCaching" to true, + "enableOptimization" to true, + "maxHistorySize" to cacheSize + ) + + return Calculator.createWithSettings(settingsMap) + } + + /** + * ๋ณด์•ˆ ๊ฐ•ํ™” ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณด์•ˆ ์„ค์ •์ด ๊ฐ•ํ™”๋œ ๊ณ„์‚ฐ๊ธฐ + */ + fun createSecureCalculator(): Calculator { + createdCalculatorCount.incrementAndGet() + + val settingsMap = mapOf( + "precision" to 10, + "strictMode" to true, + "enableCaching" to false, // ๋ณด์•ˆ์„ ์œ„ํ•ด ์บ์‹ฑ ๋น„ํ™œ์„ฑํ™” + "enableOptimization" to false, // ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ๋™์ž‘์„ ์œ„ํ•ด ์ตœ์ ํ™” ๋น„ํ™œ์„ฑํ™” + "maxHistorySize" to 10 + ) + + return Calculator.createWithSettings(settingsMap) + } + + + /** + * ํŒฉํ† ๋ฆฌ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "factoryName" to "CalculatorFactory", + "createdCalculators" to createdCalculatorCount.get(), + "createdSessions" to createdSessionCount.get(), + "createdRequests" to createdRequestCount.get(), + "supportedCalculatorTypes" to listOf("basic", "scientific", "statistical", "engineering", "custom"), + "supportedEnvironments" to listOf("default", "scientific", "engineering", "statistical"), + "cacheEnabled" to true, + "complexityLevel" to Complexity.HIGH.name + ) + + /** + * ํŒฉํ† ๋ฆฌ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "defaultPrecision" to 10, + "defaultAngleUnit" to "RADIANS", + "defaultCachingEnabled" to true, + "defaultOptimizationEnabled" to true, + "maxConcurrency" to 10, + "defaultCacheSize" to 1000, + "securityMode" to "standard" + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/BatchContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/BatchContract.kt new file mode 100644 index 00000000..09eb8bc9 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/BatchContract.kt @@ -0,0 +1,34 @@ +package hs.kr.entrydsm.domain.calculator.interfaces + +import hs.kr.entrydsm.domain.calculator.values.CalculationResult +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest + +/** + * ์ผ๊ด„ ์ฒ˜๋ฆฌ ๋ฐ ๋น„๋™๊ธฐ ๊ณ„์‚ฐ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Interface Segregation Principle์„ ์ ์šฉํ•˜์—ฌ CalculatorContract์—์„œ + * ์ผ๊ด„ ์ฒ˜๋ฆฌ ๋ฐ ๋น„๋™๊ธฐ ์ฒ˜๋ฆฌ ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋“ค๋งŒ ๋ถ„๋ฆฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.08.05 + */ +interface BatchContract { + + /** + * ์ผ๊ด„ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param requests ๊ณ„์‚ฐ ์š”์ฒญ๋“ค + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋“ค + */ + fun calculateBatch(requests: List): List + + /** + * ๋น„๋™๊ธฐ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @param callback ์™„๋ฃŒ ์ฝœ๋ฐฑ + */ + fun calculateAsync(request: CalculationRequest, callback: (CalculationResult) -> Unit) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculationContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculationContract.kt new file mode 100644 index 00000000..2186437e --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculationContract.kt @@ -0,0 +1,53 @@ +package hs.kr.entrydsm.domain.calculator.interfaces + +import hs.kr.entrydsm.domain.calculator.entities.CalculationSession +import hs.kr.entrydsm.domain.calculator.values.CalculationResult +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest + +/** + * ๊ธฐ๋ณธ ๊ณ„์‚ฐ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Interface Segregation Principle์„ ์ ์šฉํ•˜์—ฌ CalculatorContract์—์„œ + * ๊ธฐ๋ณธ ๊ณ„์‚ฐ ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋“ค๋งŒ ๋ถ„๋ฆฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.08.05 + */ +interface CalculationContract { + + /** + * ์ˆ˜์‹์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + */ + fun calculate(request: CalculationRequest): CalculationResult + + /** + * ์ˆ˜์‹ ๋ฌธ์ž์—ด์„ ์ง์ ‘ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + */ + fun calculate(expression: String): CalculationResult + + /** + * ๋ณ€์ˆ˜์™€ ํ•จ๊ป˜ ์ˆ˜์‹์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @param variables ๋ณ€์ˆ˜ ๋งต + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + */ + fun calculate(expression: String, variables: Map): CalculationResult + + /** + * ์„ธ์…˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ์™€ ์—…๋ฐ์ดํŠธ๋œ ์„ธ์…˜ + */ + fun calculateWithSession(request: CalculationRequest, session: CalculationSession): Pair +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculatorContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculatorContract.kt new file mode 100644 index 00000000..3ba8d605 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/CalculatorContract.kt @@ -0,0 +1,26 @@ +package hs.kr.entrydsm.domain.calculator.interfaces + +/** + * ๊ณ„์‚ฐ๊ธฐ์˜ ํ•ต์‹ฌ ๊ณ„์•ฝ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Interface Segregation Principle์„ ์ ์šฉํ•˜์—ฌ ๊ธฐ๋Šฅ๋ณ„๋กœ ๋ถ„๋ฆฌ๋œ ์ธํ„ฐํŽ˜์ด์Šค๋“ค์„ + * ๋ชจ๋‘ ์ƒ์†ํ•˜๋Š” ํ†ตํ•ฉ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. Anti-Corruption Layer ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜์—ฌ + * ๋‹ค์–‘ํ•œ ๊ณ„์‚ฐ๊ธฐ ๊ตฌํ˜„์ฒด๋“ค ๊ฐ„์˜ ํ˜ธํ™˜์„ฑ์„ ๋ณด์žฅํ•˜๋ฉฐ, ๊ณ„์‚ฐ๊ธฐ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ + * ํ‘œ์ค€ํ™”๋œ ๋ฐฉ์‹์œผ๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * ํด๋ผ์ด์–ธํŠธ๋Š” ํ•„์š”์— ๋”ฐ๋ผ ๊ฐœ๋ณ„ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜ + * ๋ชจ๋“  ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ด ํ†ตํ•ฉ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface CalculatorContract : + CalculationContract, + ValidationContract, + ParsingContract, + OptimizationContract, + BatchContract, + MetadataContract, + LifecycleContract \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/LifecycleContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/LifecycleContract.kt new file mode 100644 index 00000000..279ca025 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/LifecycleContract.kt @@ -0,0 +1,61 @@ +package hs.kr.entrydsm.domain.calculator.interfaces + +/** + * ๊ณ„์‚ฐ๊ธฐ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Interface Segregation Principle์„ ์ ์šฉํ•˜์—ฌ CalculatorContract์—์„œ + * ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ, ๋„์›€๋ง, ์ œ์•ฝ์‚ฌํ•ญ ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋“ค๋งŒ ๋ถ„๋ฆฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.08.05 + */ +interface LifecycleContract { + + /** + * ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun reset() + + /** + * ๊ณ„์‚ฐ๊ธฐ๊ฐ€ ํ™œ์„ฑ ์ƒํƒœ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ™œ์„ฑ ์ƒํƒœ์ด๋ฉด true + */ + fun isActive(): Boolean + + /** + * ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ข…๋ฃŒํ•ฉ๋‹ˆ๋‹ค. + */ + fun shutdown() + + /** + * ๊ณ„์‚ฐ๊ธฐ์˜ ๋ฒ„์ „ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฒ„์ „ ์ •๋ณด + */ + fun getVersion(): String + + /** + * ๊ณ„์‚ฐ๊ธฐ์˜ ๋„์›€๋ง ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„์›€๋ง ๋ฌธ์ž์—ด + */ + fun getHelp(): String + + /** + * ํŠน์ • ํ•จ์ˆ˜์˜ ๋„์›€๋ง์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param functionName ํ•จ์ˆ˜ ์ด๋ฆ„ + * @return ํ•จ์ˆ˜ ๋„์›€๋ง + */ + fun getFunctionHelp(functionName: String): String + + /** + * ๊ณ„์‚ฐ๊ธฐ์˜ ํ•œ๊ณ„์™€ ์ œ์•ฝ์‚ฌํ•ญ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ œ์•ฝ์‚ฌํ•ญ ์ •๋ณด ๋งต + */ + fun getLimitations(): Map +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/MetadataContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/MetadataContract.kt new file mode 100644 index 00000000..836f376a --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/MetadataContract.kt @@ -0,0 +1,65 @@ +package hs.kr.entrydsm.domain.calculator.interfaces + +/** + * ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฐ ์ƒํƒœ ์กฐํšŒ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Interface Segregation Principle์„ ์ ์šฉํ•˜์—ฌ CalculatorContract์—์„œ + * ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ, ์„ฑ๋Šฅ ํ†ต๊ณ„, ์„ค์ • ์ •๋ณด ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋“ค๋งŒ ๋ถ„๋ฆฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.08.05 + */ +interface MetadataContract { + + /** + * ์ง€์›๋˜๋Š” ํ•จ์ˆ˜ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ํ•จ์ˆ˜ ์ด๋ฆ„ ์ง‘ํ•ฉ + */ + fun getSupportedFunctions(): Set + + /** + * ์ง€์›๋˜๋Š” ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ์—ฐ์‚ฐ์ž ์ง‘ํ•ฉ + */ + fun getSupportedOperators(): Set + + /** + * ์ง€์›๋˜๋Š” ์ƒ์ˆ˜ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ์ƒ์ˆ˜ ๋งต + */ + fun getSupportedConstants(): Map + + /** + * ๊ณ„์‚ฐ๊ธฐ์˜ ๊ธฐ๋Šฅ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param feature ํ™•์ธํ•  ๊ธฐ๋Šฅ ์ด๋ฆ„ + * @return ์ง€์›๋˜๋ฉด true + */ + fun supportsFeature(feature: String): Boolean + + /** + * ๊ณ„์‚ฐ๊ธฐ์˜ ์„ฑ๋Šฅ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๋Šฅ ํ†ต๊ณ„ ๋งต + */ + fun getPerformanceStatistics(): Map + + /** + * ๊ณ„์‚ฐ๊ธฐ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map + + /** + * ๊ณ„์‚ฐ๊ธฐ์˜ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒํƒœ ์ •๋ณด ๋งต + */ + fun getStatus(): Map +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/OptimizationContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/OptimizationContract.kt new file mode 100644 index 00000000..f22b099e --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/OptimizationContract.kt @@ -0,0 +1,31 @@ +package hs.kr.entrydsm.domain.calculator.interfaces + +/** + * ์ˆ˜์‹ ์ตœ์ ํ™” ๊ธฐ๋Šฅ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Interface Segregation Principle์„ ์ ์šฉํ•˜์—ฌ CalculatorContract์—์„œ + * ์ตœ์ ํ™” ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋“ค๋งŒ ๋ถ„๋ฆฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.08.05 + */ +interface OptimizationContract { + + /** + * ์ˆ˜์‹์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ์ตœ์ ํ™”ํ•  ์ˆ˜์‹ + * @return ์ตœ์ ํ™”๋œ ์ˆ˜์‹ + */ + fun optimizeExpression(expression: String): String + + /** + * ์ˆ˜์‹์˜ ์˜ˆ์ƒ ์‹คํ–‰ ์‹œ๊ฐ„์„ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๋ถ„์„ํ•  ์ˆ˜์‹ + * @return ์˜ˆ์ƒ ์‹คํ–‰ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun estimateExecutionTime(expression: String): Long +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/ParsingContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/ParsingContract.kt new file mode 100644 index 00000000..97745eec --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/ParsingContract.kt @@ -0,0 +1,47 @@ +package hs.kr.entrydsm.domain.calculator.interfaces + +/** + * ์ˆ˜์‹ ํŒŒ์‹ฑ ๋ฐ ๋ถ„์„ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Interface Segregation Principle์„ ์ ์šฉํ•˜์—ฌ CalculatorContract์—์„œ + * ํŒŒ์‹ฑ ๋ฐ ๋ถ„์„ ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋“ค๋งŒ ๋ถ„๋ฆฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.08.05 + */ +interface ParsingContract { + + /** + * ์ˆ˜์‹์„ ํŒŒ์‹ฑํ•˜๊ณ  ๊ตฌ๋ฌธ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ํŒŒ์‹ฑํ•  ์ˆ˜์‹ + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ ์ •๋ณด + */ + fun parseExpression(expression: String): Map + + /** + * ์ˆ˜์‹์— ์‚ฌ์šฉ๋œ ๋ณ€์ˆ˜๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๋ถ„์„ํ•  ์ˆ˜์‹ + * @return ์‚ฌ์šฉ๋œ ๋ณ€์ˆ˜ ์ง‘ํ•ฉ + */ + fun extractVariables(expression: String): Set + + /** + * ์ˆ˜์‹์— ์‚ฌ์šฉ๋œ ํ•จ์ˆ˜๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๋ถ„์„ํ•  ์ˆ˜์‹ + * @return ์‚ฌ์šฉ๋œ ํ•จ์ˆ˜ ์ง‘ํ•ฉ + */ + fun extractFunctions(expression: String): Set + + /** + * ์ˆ˜์‹์˜ ๋ณต์žก๋„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๋ถ„์„ํ•  ์ˆ˜์‹ + * @return ๋ณต์žก๋„ ์ˆ˜์น˜ + */ + fun calculateComplexity(expression: String): Int +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/ValidationContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/ValidationContract.kt new file mode 100644 index 00000000..9974ffb4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/interfaces/ValidationContract.kt @@ -0,0 +1,32 @@ +package hs.kr.entrydsm.domain.calculator.interfaces + +/** + * ์ˆ˜์‹ ๊ฒ€์ฆ ๊ธฐ๋Šฅ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Interface Segregation Principle์„ ์ ์šฉํ•˜์—ฌ CalculatorContract์—์„œ + * ๊ฒ€์ฆ ๊ด€๋ จ ๋ฉ”์„œ๋“œ๋“ค๋งŒ ๋ถ„๋ฆฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.08.05 + */ +interface ValidationContract { + + /** + * ์ˆ˜์‹์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๊ฒ€์ฆํ•  ์ˆ˜์‹ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateExpression(expression: String): Boolean + + /** + * ์ˆ˜์‹์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค (๋ณ€์ˆ˜ ํฌํ•จ). + * + * @param expression ๊ฒ€์ฆํ•  ์ˆ˜์‹ + * @param variables ๋ณ€์ˆ˜ ๋งต + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateExpression(expression: String, variables: Map): Boolean +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/policies/CalculationPerformancePolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/policies/CalculationPerformancePolicy.kt new file mode 100644 index 00000000..003805bf --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/policies/CalculationPerformancePolicy.kt @@ -0,0 +1,589 @@ +package hs.kr.entrydsm.domain.calculator.policies + +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import hs.kr.entrydsm.domain.calculator.values.CalculationResult +import hs.kr.entrydsm.domain.calculator.values.MultiStepCalculationRequest +import hs.kr.entrydsm.domain.calculator.values.PerformanceRecommendation +import hs.kr.entrydsm.domain.calculator.values.RecommendationType +import hs.kr.entrydsm.domain.calculator.values.RecommendationPriority +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope +import hs.kr.entrydsm.global.constants.ErrorCodes +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong +import kotlin.system.measureTimeMillis +import kotlinx.coroutines.* +import java.util.concurrent.TimeoutException +import java.util.concurrent.ConcurrentLinkedDeque +import java.util.concurrent.atomic.AtomicInteger +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * POC ์ฝ”๋“œ์˜ ์„ฑ๋Šฅ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ์„ DDD Policy ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * POC ์ฝ”๋“œ์˜ CalculatorService์—์„œ ์ œ๊ณตํ•˜๋Š” ์บ์‹ฑ(@Cacheable), ์„ฑ๋Šฅ ์ธก์ •(measureTimeMillis), + * ์‹คํ–‰ ์‹œ๊ฐ„ ์ถ”์ , ๋ฉ”๋ชจ๋ฆฌ ๊ด€๋ฆฌ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ •์ฑ…์œผ๋กœ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * Spring Boot์˜ @Cacheable๊ณผ ๋™์ผํ•œ ๊ธฐ๋Šฅ์„ DDD ์ •์ฑ…์œผ๋กœ ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.28 + */ +@Policy( + name = "CalculationPerformance", + description = "POC ์ฝ”๋“œ ๊ธฐ๋ฐ˜์˜ ๊ณ„์‚ฐ ์„ฑ๋Šฅ ๊ด€๋ฆฌ ์ •์ฑ…", + domain = "calculator", + scope = Scope.DOMAIN +) +class CalculationPerformancePolicy { + + /** + * Thread-safe circular FIFO queue implementation + * ๊ณ ์ • ํฌ๊ธฐ ์›ํ˜• ํ๋กœ ์ž๋™์œผ๋กœ ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ฑฐํ•˜๋ฉฐ ๋™์‹œ์„ฑ ์„ฑ๋Šฅ์„ ๊ฐœ์„ ํ•ฉ๋‹ˆ๋‹ค. + */ + private class ThreadSafeCircularQueue(private val maxSize: Int) { + private val deque = ConcurrentLinkedDeque() + private val size = AtomicInteger(0) + + fun add(element: T) { + deque.addLast(element) + val currentSize = size.incrementAndGet() + + // ํฌ๊ธฐ ์ œํ•œ ์ดˆ๊ณผ ์‹œ ์˜ค๋ž˜๋œ ์š”์†Œ ์ œ๊ฑฐ + if (currentSize > maxSize) { + deque.pollFirst() + size.decrementAndGet() + } + } + + fun toList(): List = deque.toList() + + fun size(): Int = size.get() + + fun clear() { + deque.clear() + size.set(0) + } + + fun isEmpty(): Boolean = deque.isEmpty() + + fun average(): Double { + val list = toList() + return if (list.isEmpty()) 0.0 + else list.map { (it as Number).toDouble() }.average() + } + } + + companion object { + // POC ์ฝ”๋“œ์˜ CalculatorProperties ๊ธฐ๋ณธ๊ฐ’๋“ค + private const val DEFAULT_CACHE_TTL_SECONDS = 3600L + private const val DEFAULT_MAX_EXECUTION_TIME_MS = 30000L + private const val DEFAULT_MAX_MEMORY_MB = 100 + private const val DEFAULT_MAX_CACHE_SIZE = 1000 + + // ์„ฑ๋Šฅ ์ž„๊ณ„๊ฐ’๋“ค + private const val SLOW_CALCULATION_THRESHOLD_MS = 1000L + private const val VERY_SLOW_CALCULATION_THRESHOLD_MS = 5000L + private const val MEMORY_WARNING_THRESHOLD_MB = 80 + + // ํ†ต๊ณ„ ์ถ”์  + private val totalCalculations = AtomicLong(0) + private val cachedCalculations = AtomicLong(0) + private val slowCalculations = AtomicLong(0) + private val failedCalculations = AtomicLong(0) + private val totalExecutionTime = AtomicLong(0) + + private const val POLICY_NAME = "CalculationPerformancePolicy" + private const val BASED_ON = "POC_CalculatorService_Performance" + + private val DEFAULT_FEATURES = listOf( + "caching", + "performance_monitoring", + "timeout_handling", + "memory_management" + ) + } + + // POC ์ฝ”๋“œ์˜ @Cacheable ๊ธฐ๋Šฅ์„ ๊ตฌํ˜„ํ•˜๋Š” ์บ์‹œ + private val calculationCache = ConcurrentHashMap() + private val multiStepCache = ConcurrentHashMap() + + // ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง์„ ์œ„ํ•œ ๋ฉ”ํŠธ๋ฆญ (Thread-safe circular queues) + private val executionTimes = ThreadSafeCircularQueue(1000) + private val memoryUsage = ThreadSafeCircularQueue(1000) + + /** + * POC ์ฝ”๋“œ์˜ CalculatorService.calculate์— ํ•ด๋‹นํ•˜๋Š” ์„ฑ๋Šฅ ์ •์ฑ… ์ ์šฉ + */ + fun applyPerformancePolicy( + request: CalculationRequest, + enableCaching: Boolean = true, + timeout: Long = DEFAULT_MAX_EXECUTION_TIME_MS, + operation: () -> CalculationResult + ): CalculationResult { + totalCalculations.incrementAndGet() + + // ์บ์‹œ ํ™•์ธ (POC์˜ @Cacheable ๊ธฐ๋Šฅ) + if (enableCaching) { + val cacheKey = generateCacheKey(request) + val cachedResult = calculationCache[cacheKey] + + if (cachedResult != null && !cachedResult.isExpired()) { + cachedCalculations.incrementAndGet() + return cachedResult.result.copy( + executionTimeMs = 0L, + metadata = cachedResult.result.metadata + mapOf( + "cached" to true, + "cacheHit" to true + ) + ) + } + } + + // ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ฒดํฌ + val beforeMemory = getUsedMemoryMB() + if (beforeMemory > MEMORY_WARNING_THRESHOLD_MB) { + return createMemoryLimitError(request, beforeMemory) + } + + // ํƒ€์ž„์•„์›ƒ๊ณผ ์„ฑ๋Šฅ ์ธก์ • (POC์˜ measureTimeMillis ๊ธฐ๋Šฅ) + var result: CalculationResult + val executionTime = measureTimeMillis { + try { + result = executeWithTimeout(operation, timeout) + } catch (e: Exception) { + failedCalculations.incrementAndGet() + result = createExecutionError(request, e) + } + } + + // ์‹คํ–‰ ์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ (POC ์ฝ”๋“œ์˜ executionTimeMs ์„ค์ •) + result = result.copy(executionTimeMs = executionTime) + + // ์„ฑ๋Šฅ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + updatePerformanceMetrics(executionTime, getUsedMemoryMB() - beforeMemory) + + // ๋А๋ฆฐ ๊ณ„์‚ฐ ๊ฐ์ง€ + if (executionTime > SLOW_CALCULATION_THRESHOLD_MS) { + slowCalculations.incrementAndGet() + handleSlowCalculation(request, executionTime) + } + + // ์บ์‹œ ์ €์žฅ (์„ฑ๊ณตํ•œ ๊ฒฝ์šฐ๋งŒ) + if (enableCaching && result.isSuccess()) { + val cacheKey = generateCacheKey(request) + calculationCache[cacheKey] = CachedResult(result, System.currentTimeMillis()) + + // ์บ์‹œ ํฌ๊ธฐ ๊ด€๋ฆฌ + if (calculationCache.size > DEFAULT_MAX_CACHE_SIZE) { + evictOldestCacheEntries() + } + } + + return result + } + + /** + * POC ์ฝ”๋“œ์˜ CalculatorService.calculateMultiStep์— ํ•ด๋‹นํ•˜๋Š” ์„ฑ๋Šฅ ์ •์ฑ… ์ ์šฉ + */ + fun applyMultiStepPerformancePolicy( + request: MultiStepCalculationRequest, + enableCaching: Boolean = true, + timeout: Long = DEFAULT_MAX_EXECUTION_TIME_MS, + operation: () -> CalculationResult + ): CalculationResult { + totalCalculations.incrementAndGet() + + // ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ์บ์‹œ ํ™•์ธ + if (enableCaching) { + val cacheKey = generateMultiStepCacheKey(request) + val cachedResult = multiStepCache[cacheKey] + + if (cachedResult != null && !cachedResult.isExpired()) { + cachedCalculations.incrementAndGet() + return cachedResult.result.copy( + executionTimeMs = 0L, + metadata = cachedResult.result.metadata + mapOf("cached" to true) + ) + } + } + + // ๋‹จ๊ณ„๋ณ„ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง + val stepExecutionTimes = mutableListOf() + var totalResult: CalculationResult + + val totalExecutionTime = measureTimeMillis { + try { + totalResult = executeMultiStepWithTimeout(request, operation, timeout) { stepTime -> + stepExecutionTimes.add(stepTime) + } + } catch (e: Exception) { + failedCalculations.incrementAndGet() + totalResult = createMultiStepExecutionError(request, e) + } + } + + // ๊ฒฐ๊ณผ ์—…๋ฐ์ดํŠธ + totalResult = totalResult.copy( + executionTimeMs = totalExecutionTime, + metadata = totalResult.metadata + mapOf("stepExecutionTimes" to stepExecutionTimes) + ) + + // ์„ฑ๋Šฅ ํ†ต๊ณ„ ์—…๋ฐ์ดํŠธ + updatePerformanceMetrics(totalExecutionTime, 0L) + + // ์บ์‹œ ์ €์žฅ + if (enableCaching && totalResult.isSuccess()) { + val cacheKey = generateMultiStepCacheKey(request) + multiStepCache[cacheKey] = CachedMultiStepResult(totalResult, System.currentTimeMillis()) + } + + return totalResult + } + + /** + * ๊ณ„์‚ฐ ์„ฑ๋Šฅ์ด ์ •์ฑ…์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์ฆ + */ + fun meetsPerformanceRequirements( + executionTime: Long, + memoryUsage: Long, + cacheHitRate: Double + ): Boolean { + return executionTime < DEFAULT_MAX_EXECUTION_TIME_MS && + memoryUsage < DEFAULT_MAX_MEMORY_MB && + cacheHitRate >= 0.8 // 80% ์ด์ƒ์˜ ์บ์‹œ ํžˆํŠธ์œจ ์š”๊ตฌ + } + + /** + * ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ถŒ์žฅ์‚ฌํ•ญ ์ œ๊ณต + */ + fun getPerformanceRecommendations(): List { + val recommendations = mutableListOf() + + val avgExecutionTime = if (!executionTimes.isEmpty()) { + executionTimes.average() + } else 0.0 + + val cacheHitRate = if (totalCalculations.get() > 0) { + cachedCalculations.get().toDouble() / totalCalculations.get() + } else 0.0 + + if (avgExecutionTime > SLOW_CALCULATION_THRESHOLD_MS) { + recommendations.add( + PerformanceRecommendation( + type = RecommendationType.EXECUTION_TIME, + message = "ํ‰๊ท  ์‹คํ–‰ ์‹œ๊ฐ„์ด ${avgExecutionTime.toLong()}ms์ž…๋‹ˆ๋‹ค. ์ˆ˜์‹ ๋ณต์žก๋„๋ฅผ ์ค„์ด๊ฑฐ๋‚˜ ์บ์‹ฑ์„ ํ™œ์„ฑํ™”ํ•˜์„ธ์š”.", + priority = if (avgExecutionTime > VERY_SLOW_CALCULATION_THRESHOLD_MS) + RecommendationPriority.HIGH else RecommendationPriority.MEDIUM + ) + ) + } + + if (cacheHitRate < 0.5) { + recommendations.add( + PerformanceRecommendation( + type = RecommendationType.CACHE_HIT_RATE, + message = "์บ์‹œ ํžˆํŠธ์œจ์ด ${(cacheHitRate * 100).toInt()}%์ž…๋‹ˆ๋‹ค. ์บ์‹œ ์„ค์ •์„ ์ตœ์ ํ™”ํ•˜์„ธ์š”.", + priority = RecommendationPriority.MEDIUM + ) + ) + } + + val avgMemory = if (!memoryUsage.isEmpty()) { + memoryUsage.average() + } else 0.0 + + if (avgMemory > MEMORY_WARNING_THRESHOLD_MB) { + recommendations.add( + PerformanceRecommendation( + type = RecommendationType.MEMORY_USAGE, + message = "ํ‰๊ท  ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ${avgMemory.toLong()}MB์ž…๋‹ˆ๋‹ค. ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ์„ ์ตœ์ ํ™”ํ•˜์„ธ์š”.", + priority = RecommendationPriority.HIGH + ) + ) + } + + return recommendations + } + + /** + * ์บ์‹œ ํ†ต๊ณ„ ์ •๋ณด ๋ฐ˜ํ™˜ (POC ์ฝ”๋“œ์˜ ํŒŒ์„œ ์ •๋ณด ์กฐํšŒ์™€ ์œ ์‚ฌ) + */ + fun getCacheStatistics(): Map { + val cacheHitRate = if (totalCalculations.get() > 0) { + cachedCalculations.get().toDouble() / totalCalculations.get() + } else 0.0 + + return mapOf( + "totalCalculations" to totalCalculations.get(), + "cachedCalculations" to cachedCalculations.get(), + "cacheHitRate" to cacheHitRate, + "cacheSize" to calculationCache.size, + "multiStepCacheSize" to multiStepCache.size, + "slowCalculations" to slowCalculations.get(), + "failedCalculations" to failedCalculations.get(), + "averageExecutionTime" to if (!executionTimes.isEmpty()) executionTimes.average() else 0.0, + "totalExecutionTime" to totalExecutionTime.get() + ) + } + + /** + * ์บ์‹œ ์ •๋ฆฌ ์ •์ฑ… ์ ์šฉ + */ + fun applyCacheEvictionPolicy() { + val currentTime = System.currentTimeMillis() + val expiredKeys = mutableListOf() + + // ๋งŒ๋ฃŒ๋œ ์บ์‹œ ํ•ญ๋ชฉ ์ฐพ๊ธฐ + calculationCache.forEach { (key, cachedResult) -> + if (cachedResult.isExpired(currentTime)) { + expiredKeys.add(key) + } + } + + // ๋งŒ๋ฃŒ๋œ ํ•ญ๋ชฉ ์ œ๊ฑฐ + expiredKeys.forEach { key -> + calculationCache.remove(key) + } + + // ๋‹ค๋‹จ๊ณ„ ์บ์‹œ๋„ ๋™์ผํ•˜๊ฒŒ ์ฒ˜๋ฆฌ + val expiredMultiStepKeys = mutableListOf() + multiStepCache.forEach { (key, cachedResult) -> + if (cachedResult.isExpired(currentTime)) { + expiredMultiStepKeys.add(key) + } + } + + expiredMultiStepKeys.forEach { key -> + multiStepCache.remove(key) + } + } + + // Private helper methods + + private fun generateCacheKey(request: CalculationRequest): String { + return "${request.formula}:${request.variables.hashCode()}" + } + + private fun generateMultiStepCacheKey(request: MultiStepCalculationRequest): String { + return "${request.steps.hashCode()}:${request.variables.hashCode()}" + } + + private fun executeWithTimeout(operation: () -> CalculationResult, timeout: Long): CalculationResult { + return try { + runBlocking { + withTimeout(timeout) { + operation() + } + } + } catch (e: TimeoutCancellationException) { + CalculationResult( + formula = "timeout", + result = null, + executionTimeMs = timeout, + errors = listOf("๊ณ„์‚ฐ์ด ์ œํ•œ ์‹œ๊ฐ„(${timeout}ms)์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + metadata = mapOf( + "timeout" to true, + "timeoutMs" to timeout + ) + ) + } + } + + private fun executeMultiStepWithTimeout( + request: MultiStepCalculationRequest, + operation: () -> CalculationResult, + timeout: Long, + stepTimeCallback: (Long) -> Unit + ): CalculationResult { + return try { + runBlocking { + withTimeout(timeout) { + val startTime = System.currentTimeMillis() + var totalStepTime = 0L + + // ๊ฐ ๋‹จ๊ณ„๋ณ„ ์‹œ๊ฐ„ ์ธก์ • ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + request.steps.forEachIndexed { index, step -> + val stepStartTime = System.currentTimeMillis() + + // ๋‹จ๊ณ„๋ณ„ ์‹คํ–‰ ์‹œ๊ฐ„ ์ฒดํฌ (์ „์ฒด ํƒ€์ž„์•„์›ƒ์˜ ์ผ๋ถ€) + val remainingTime = timeout - totalStepTime + if (remainingTime <= 0) { + throw CalculatorException.stepTimeoutExceeded(index, remainingTime) + } + + // ๋‹จ๊ณ„ ์‹คํ–‰ ์‹œ๊ฐ„ ์ธก์ • (์‹ค์ œ๋กœ๋Š” ๊ฐ ๋‹จ๊ณ„๋ฅผ ์‹คํ–‰) + delay(1) // ์‹คํ–‰ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + + val stepExecutionTime = System.currentTimeMillis() - stepStartTime + totalStepTime += stepExecutionTime + + // ๋‹จ๊ณ„๋ณ„ ์‹œ๊ฐ„ ์ฝœ๋ฐฑ ํ˜ธ์ถœ + stepTimeCallback(stepExecutionTime) + + // ๋‹จ๊ณ„๋ณ„ ๋กœ๊น… + if (stepExecutionTime > SLOW_CALCULATION_THRESHOLD_MS / request.steps.size) { + throw DomainException( + errorCode = ErrorCode.PERFORMANCE_WARNING, + message = "๋А๋ฆฐ ๋‹จ๊ณ„ ๊ฐ์ง€: ๋‹จ๊ณ„ $index (${step.formula.substring(0, minOf(step.formula.length, 50))})๊ฐ€ ${stepExecutionTime}ms ์†Œ์š”๋จ", + context = mapOf( + "stepIndex" to index, + "stepFormula" to step.formula, + "executionTime" to stepExecutionTime, + "threshold" to (SLOW_CALCULATION_THRESHOLD_MS / request.steps.size), + "multiStep" to true + ) + ) + } + } + + // ์‹ค์ œ ์—ฐ์‚ฐ ์‹คํ–‰ + operation() + } + } + } catch (e: TimeoutCancellationException) { + CalculationResult( + formula = request.steps.joinToString("; ") { it.formula }, + result = null, + executionTimeMs = timeout, + errors = listOf("๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ์ด ์ œํ•œ ์‹œ๊ฐ„(${timeout}ms)์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + metadata = mapOf( + "timeout" to true, + "timeoutMs" to timeout, + "multiStep" to true, + "totalSteps" to request.steps.size + ) + ) + } + } + + private fun updatePerformanceMetrics(executionTime: Long, memoryDelta: Long) { + // Thread-safe circular queues automatically handle size limits + executionTimes.add(executionTime) + memoryUsage.add(memoryDelta) + + totalExecutionTime.addAndGet(executionTime) + } + + private fun handleSlowCalculation(request: CalculationRequest, executionTime: Long) { + // ๋А๋ฆฐ ๊ณ„์‚ฐ์— ๋Œ€ํ•œ ๊ธ€๋กœ๋ฒŒ ๋„๋ฉ”์ธ ์˜ˆ์™ธ ๋ฐœ์ƒ + throw DomainException( + errorCode = ErrorCode.PERFORMANCE_WARNING, + message = "๋А๋ฆฐ ๊ณ„์‚ฐ ๊ฐ์ง€: '${request.formula}'๊ฐ€ ${executionTime}ms ์†Œ์š”๋จ", + context = mapOf( + "formula" to request.formula, + "executionTime" to executionTime, + "threshold" to SLOW_CALCULATION_THRESHOLD_MS, + "performanceIssue" to true + ) + ) + } + + private fun evictOldestCacheEntries() { + synchronized(calculationCache) { + val sortedEntries = calculationCache.entries.sortedBy { it.value.timestamp } + val toRemove = sortedEntries.take(calculationCache.size - DEFAULT_MAX_CACHE_SIZE + 100) + + toRemove.forEach { entry -> + calculationCache.remove(entry.key) + } + } + } + + private fun getUsedMemoryMB(): Long { + val runtime = Runtime.getRuntime() + val totalMemory = runtime.totalMemory() + val freeMemory = runtime.freeMemory() + return (totalMemory - freeMemory) / (1024 * 1024) + } + + private fun createMemoryLimitError(request: CalculationRequest, memoryUsage: Long): CalculationResult { + return CalculationResult( + formula = request.formula, + result = null, + executionTimeMs = 0L, + errors = listOf("๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ํ•œ๊ณ„ ์ดˆ๊ณผ: ${memoryUsage}MB"), + metadata = mapOf("errorCode" to ErrorCodes.Calculator.MEMORY_LIMIT_EXCEEDED) + ) + } + + private fun createExecutionError(request: CalculationRequest, exception: Exception): CalculationResult { + return CalculationResult( + formula = request.formula, + result = null, + executionTimeMs = 0L, + errors = listOf("์‹คํ–‰ ์˜ค๋ฅ˜: ${exception.message}"), + metadata = mapOf("errorCode" to ErrorCodes.Calculator.CALCULATION_FAILED) + ) + } + + private fun createMultiStepExecutionError( + request: MultiStepCalculationRequest, + exception: Exception + ): CalculationResult { + return CalculationResult( + result = null, + executionTimeMs = 0L, + formula = "MultiStep", + errors = listOf("๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ์‹คํ–‰ ์˜ค๋ฅ˜: ${exception.message}"), + metadata = mapOf("errorCode" to ErrorCodes.Calculator.CALCULATION_FAILED) + ) + } + + // Data classes + + private data class CachedResult( + val result: CalculationResult, + val timestamp: Long, + val ttl: Long = DEFAULT_CACHE_TTL_SECONDS * 1000 + ) { + fun isExpired(currentTime: Long = System.currentTimeMillis()): Boolean { + return currentTime - timestamp > ttl + } + } + + private data class CachedMultiStepResult( + val result: CalculationResult, + val timestamp: Long, + val ttl: Long = DEFAULT_CACHE_TTL_SECONDS * 1000 + ) { + fun isExpired(currentTime: Long = System.currentTimeMillis()): Boolean { + return currentTime - timestamp > ttl + } + } + + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getConfiguration(): Map = mapOf( + "name" to POLICY_NAME, + "based_on" to BASED_ON, + "defaultCacheTtlSeconds" to DEFAULT_CACHE_TTL_SECONDS, + "defaultMaxExecutionTimeMs" to DEFAULT_MAX_EXECUTION_TIME_MS, + "defaultMaxMemoryMB" to DEFAULT_MAX_MEMORY_MB, + "defaultMaxCacheSize" to DEFAULT_MAX_CACHE_SIZE, + "slowCalculationThresholdMs" to SLOW_CALCULATION_THRESHOLD_MS, + "memoryWarningThresholdMB" to MEMORY_WARNING_THRESHOLD_MB, + "features" to DEFAULT_FEATURES + ) + + /** + * ์ •์ฑ…์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getStatistics(): Map = mapOf( + "policyName" to POLICY_NAME, + "totalCalculations" to totalCalculations.get(), + "cachedCalculations" to cachedCalculations.get(), + "slowCalculations" to slowCalculations.get(), + "failedCalculations" to failedCalculations.get(), + "totalExecutionTime" to totalExecutionTime.get(), + "cacheSize" to calculationCache.size, + "multiStepCacheSize" to multiStepCache.size, + "pocCompatibility" to true + ) +} + diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/policies/CalculationPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/policies/CalculationPolicy.kt new file mode 100644 index 00000000..1a597ba1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/policies/CalculationPolicy.kt @@ -0,0 +1,446 @@ +package hs.kr.entrydsm.domain.calculator.policies + +import hs.kr.entrydsm.domain.calculator.entities.CalculationSession +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * ๊ณ„์‚ฐ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Policy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๊ณ„์‚ฐ ๊ณผ์ •์—์„œ ์ ์šฉ๋˜๋Š” + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™๊ณผ ์ •์ฑ…์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ๋ณด์•ˆ, ์„ฑ๋Šฅ, ์ •ํ™•์„ฑ๊ณผ + * ๊ด€๋ จ๋œ ๊ณ„์‚ฐ ์ •์ฑ…์„ ์ค‘์•™ ์ง‘์ค‘์‹์œผ๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Policy( + name = "Calculation", + description = "๊ณ„์‚ฐ ๊ณผ์ •์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™๊ณผ ์ •์ฑ…์„ ๊ด€๋ฆฌ", + domain = "calculator", + scope = Scope.DOMAIN +) +class CalculationPolicy { + + companion object { + private const val MAX_EXPRESSION_LENGTH = 10000 + private const val MAX_CALCULATION_TIME_MS = 30000L + private const val MAX_MEMORY_USAGE_MB = 100 + private const val MAX_RECURSION_DEPTH = 100 + private const val MAX_VARIABLES_PER_SESSION = 1000 + private const val MAX_SESSION_DURATION_HOURS = 24 + private const val MAX_CALCULATIONS_PER_MINUTE = 1000 + private const val MAX_CONCURRENT_CALCULATIONS = 10 + + // ํ—ˆ์šฉ๋œ ํ‘œํ˜„์‹ ํŒจํ„ด + private val ALLOWED_EXPRESSION_PATTERNS = listOf( + Regex("^[\\d\\s+\\-*/().,a-zA-Z_]+$"), // ๊ธฐ๋ณธ ์ˆ˜์‹ ํŒจํ„ด + Regex("^[\\w\\s+\\-*/.(),=<>!&|^%]+$") // ํ™•์žฅ ์ˆ˜์‹ ํŒจํ„ด + ) + + // ๊ธˆ์ง€๋œ ํŒจํ„ด๋“ค + private val FORBIDDEN_PATTERNS = listOf( + Regex("\\beval\\b", RegexOption.IGNORE_CASE), + Regex("\\bexec\\b", RegexOption.IGNORE_CASE), + Regex("\\bsystem\\b", RegexOption.IGNORE_CASE), + Regex("\\bprocess\\b", RegexOption.IGNORE_CASE), + Regex("\\bfile\\b", RegexOption.IGNORE_CASE), + Regex("\\bimport\\b", RegexOption.IGNORE_CASE), + Regex("__.*__") // Python dunder methods + ) + + private const val POLICY_NAME = "CalculationPolicy" + + private val SECURITY_RULES = listOf( + "expression_patterns", + "resource_limits", + "rate_limiting" + ) + + private val PERFORMANCE_RULES = listOf( + "execution_time", + "memory_usage", + "concurrency_limits" + ) + } + + private val sessionMetrics = mutableMapOf() + private val rateLimiters = mutableMapOf() + + /** + * ๊ณ„์‚ฐ ์š”์ฒญ์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ + * @return ํ—ˆ์šฉ๋˜๋ฉด true + */ + fun isCalculationAllowed(request: CalculationRequest, session: CalculationSession): Boolean { + return try { + validateExpressionSafety(request.formula) && + validateExpressionLength(request.formula) && + validateSessionLimits(session) && + validateRateLimit(session.sessionId) && + validateResourceUsage(request, session) + } catch (e: DomainException) { + // ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋Š” ์ด๋ฏธ ์ ์ ˆํ•œ ์—๋Ÿฌ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๋ฅผ ๊ฐ€์ง€๊ณ  ์žˆ์Œ + throw DomainException( + errorCode = ErrorCode.BUSINESS_RULE_VIOLATION, + message = "๊ณ„์‚ฐ ์ •์ฑ… ๊ฒ€์ฆ ์‹คํŒจ: ${e.message}", + cause = e, + context = mapOf( + "formula" to request.formula, + "sessionId" to session.sessionId, + "originalErrorCode" to e.getCode(), + "originalDomain" to e.getDomain(), + "validationPhase" to "calculationAllowed" + ) + ) + } catch (e: Exception) { + // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์‹œ์Šคํ…œ ์˜ˆ์™ธ + throw DomainException( + errorCode = ErrorCode.UNEXPECTED_ERROR, + message = "๊ณ„์‚ฐ ์ •์ฑ… ๊ฒ€์ฆ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "formula" to request.formula, + "sessionId" to session.sessionId, + "exceptionType" to e.javaClass.simpleName, + "validationPhase" to "calculationAllowed" + ) + ) + } + } + + /** + * ํ‘œํ˜„์‹์˜ ์•ˆ์ „์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๊ฒ€์ฆํ•  ํ‘œํ˜„์‹ + * @return ์•ˆ์ „ํ•˜๋ฉด true + */ + fun isExpressionSafe(expression: String): Boolean { + return validateExpressionSafety(expression) + } + + /** + * ๊ณ„์‚ฐ ๋ณต์žก๋„๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๊ฒ€์ฆํ•  ํ‘œํ˜„์‹ + * @return ํ—ˆ์šฉ ๊ฐ€๋Šฅํ•œ ๋ณต์žก๋„๋ฉด true + */ + fun isComplexityAcceptable(expression: String): Boolean { + val complexity = calculateComplexity(expression) + return complexity <= 10000 // ๋ณต์žก๋„ ์ œํ•œ + } + + /** + * ์„ธ์…˜์˜ ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param session ๊ฒ€์ฆํ•  ์„ธ์…˜ + * @return ๋ฆฌ์†Œ์Šค ์‚ฌ์šฉ๋Ÿ‰์ด ์ ์ ˆํ•˜๋ฉด true + */ + fun isResourceUsageAcceptable(session: CalculationSession): Boolean { + val metrics = getSessionMetrics(session.sessionId) + return metrics.memoryUsageMB <= MAX_MEMORY_USAGE_MB && + metrics.averageCalculationTimeMs <= MAX_CALCULATION_TIME_MS + } + + /** + * ๊ณ„์‚ฐ ๊ฒฐ๊ณผ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ๊ฒ€์ฆํ•  ๊ฒฐ๊ณผ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isResultValid(result: Any?): Boolean { + return when (result) { + null -> false + is Double -> !result.isNaN() && result.isFinite() + is Float -> !result.isNaN() && result.isFinite() + is Number -> true + is Boolean -> true + is String -> result.length <= 10000 + else -> false + } + } + + /** + * ์„ธ์…˜์˜ ๋ณด์•ˆ ์ •์ฑ…์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param session ๊ฒ€์ฆํ•  ์„ธ์…˜ + * @return ๋ณด์•ˆ ์ •์ฑ…์„ ๋งŒ์กฑํ•˜๋ฉด true + */ + fun applySecurityPolicy(session: CalculationSession): Boolean { + return session.variables.size <= MAX_VARIABLES_PER_SESSION && + isSessionDurationAcceptable(session) && + !containsSuspiciousActivity(session) + } + + /** + * ์„ฑ๋Šฅ ์ •์ฑ…์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ + * @return ์„ฑ๋Šฅ ์ •์ฑ…์„ ๋งŒ์กฑํ•˜๋ฉด true + */ + fun applyPerformancePolicy(request: CalculationRequest, session: CalculationSession): Boolean { + return estimateExecutionTime(request.formula) <= MAX_CALCULATION_TIME_MS && + estimateMemoryUsage(request.formula) <= MAX_MEMORY_USAGE_MB && + validateConcurrencyLimit(session.sessionId) + } + + /** + * ๊ณ„์‚ฐ ์บ์‹ฑ์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ํ‘œํ˜„์‹ + * @param variables ๋ณ€์ˆ˜๋“ค + * @return ์บ์‹ฑ ํ—ˆ์šฉ๋˜๋ฉด true + */ + fun isCachingAllowed(expression: String, variables: Map): Boolean { + return expression.length <= 1000 && // ์งง์€ ํ‘œํ˜„์‹๋งŒ ์บ์‹ฑ + variables.size <= 10 && // ๋ณ€์ˆ˜๊ฐ€ ๋งŽ์ง€ ์•Š์„ ๋•Œ๋งŒ + !containsRandomFunction(expression) && // ๋žœ๋ค ํ•จ์ˆ˜๊ฐ€ ์—†์„ ๋•Œ๋งŒ + !containsTimeFunction(expression) // ์‹œ๊ฐ„ ๊ด€๋ จ ํ•จ์ˆ˜๊ฐ€ ์—†์„ ๋•Œ๋งŒ + } + + /** + * ๊ณ„์‚ฐ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ + * @return ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + */ + fun calculatePriority(request: CalculationRequest, session: CalculationSession): Int { + var priority = 0 + + // ์‚ฌ์šฉ์ž ํƒ€์ž…์— ๋”ฐ๋ฅธ ์šฐ์„ ์ˆœ์œ„ + if (session.userId != null) { + priority += 10 // ๋กœ๊ทธ์ธ ์‚ฌ์šฉ์ž + } + + // ํ‘œํ˜„์‹ ๋ณต์žก๋„์— ๋”ฐ๋ฅธ ์šฐ์„ ์ˆœ์œ„ (๋‹จ์ˆœํ• ์ˆ˜๋ก ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + val complexity = calculateComplexity(request.formula) + priority += maxOf(0, 100 - complexity / 100) + + // ์„ธ์…˜ ํ™œ๋™์— ๋”ฐ๋ฅธ ์šฐ์„ ์ˆœ์œ„ + val sessionAge = System.currentTimeMillis() - session.createdAt.toEpochMilli() + if (sessionAge < 300000) { // 5๋ถ„ ์ด๋‚ด ์ƒ์„ฑ๋œ ์„ธ์…˜ + priority += 5 + } + + return priority + } + + /** + * ๊ณ„์‚ฐ ํƒ€์ž„์•„์›ƒ์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ํ‘œํ˜„์‹ + * @return ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun calculateTimeout(expression: String): Long { + val complexity = calculateComplexity(expression) + val baseTimeout = 1000L // ๊ธฐ๋ณธ 1์ดˆ + val complexityTimeout = (complexity / 100) * 1000L // ๋ณต์žก๋„์— ๋”ฐ๋ฅธ ์ถ”๊ฐ€ ์‹œ๊ฐ„ + return minOf(baseTimeout + complexityTimeout, MAX_CALCULATION_TIME_MS) + } + + // Private helper methods + + private fun validateExpressionSafety(expression: String): Boolean { + // ๊ธˆ์ง€๋œ ํŒจํ„ด ๊ฒ€์‚ฌ + if (FORBIDDEN_PATTERNS.any { it.containsMatchIn(expression) }) { + return false + } + + // ํ—ˆ์šฉ๋œ ํŒจํ„ด ๊ฒ€์‚ฌ + if (ALLOWED_EXPRESSION_PATTERNS.none { it.matches(expression) }) { + return false + } + + return true + } + + private fun validateExpressionLength(expression: String): Boolean { + return expression.length <= MAX_EXPRESSION_LENGTH + } + + private fun validateSessionLimits(session: CalculationSession): Boolean { + return session.variables.size <= MAX_VARIABLES_PER_SESSION && + session.calculations.size <= 10000 // ์ตœ๋Œ€ ๊ณ„์‚ฐ ์ด๋ ฅ + } + + private fun validateRateLimit(sessionId: String): Boolean { + val rateLimiter = rateLimiters.getOrPut(sessionId) { + RateLimiter(MAX_CALCULATIONS_PER_MINUTE, 60000) // 1๋ถ„๋‹น ์ตœ๋Œ€ ๊ณ„์‚ฐ ์ˆ˜ + } + return rateLimiter.isAllowed() + } + + private fun validateResourceUsage(request: CalculationRequest, session: CalculationSession): Boolean { + val estimatedTime = estimateExecutionTime(request.formula) + val estimatedMemory = estimateMemoryUsage(request.formula) + + return estimatedTime <= MAX_CALCULATION_TIME_MS && + estimatedMemory <= MAX_MEMORY_USAGE_MB + } + + private fun validateConcurrencyLimit(sessionId: String): Boolean { + val metrics = getSessionMetrics(sessionId) + return metrics.concurrentCalculations < MAX_CONCURRENT_CALCULATIONS + } + + private fun isSessionDurationAcceptable(session: CalculationSession): Boolean { + val durationHours = (System.currentTimeMillis() - session.createdAt.toEpochMilli()) / 3600000 + return durationHours <= MAX_SESSION_DURATION_HOURS + } + + private fun containsSuspiciousActivity(session: CalculationSession): Boolean { + val recentFailures = session.calculations.filter { it.isFailure() } + + // ์ตœ๊ทผ ๊ณ„์‚ฐ ์ˆ˜๊ฐ€ ๋งŽ๊ณ  ์‹คํŒจ์œจ์ด ๋†’์œผ๋ฉด ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™์œผ๋กœ ๊ฐ„์ฃผ + return recentFailures.size > 100 && session.calculations.size > 200 + } + + private fun calculateComplexity(expression: String): Int { + var complexity = expression.length // ๊ธฐ๋ณธ ๋ณต์žก๋„ + + // ์—ฐ์‚ฐ์ž ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += expression.count { it in "+-*/%^" } * 2 + + // ๊ด„ํ˜ธ ๊นŠ์ด์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + var depth = 0 + var maxDepth = 0 + for (char in expression) { + when (char) { + '(' -> { + depth++ + maxDepth = maxOf(maxDepth, depth) + } + ')' -> depth-- + } + } + complexity += maxDepth * 10 + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + val functionCount = Regex("[a-zA-Z]\\w*\\(").findAll(expression).count() + complexity += functionCount * 20 + + return complexity + } + + private fun estimateExecutionTime(expression: String): Long { + val complexity = calculateComplexity(expression) + return (complexity * 0.1).toLong() // ๋งค์šฐ ๊ฐ„๋‹จํ•œ ์ถ”์ • + } + + private fun estimateMemoryUsage(expression: String): Int { + val complexity = calculateComplexity(expression) + return (complexity * 0.001).toInt() + 1 // ๋งค์šฐ ๊ฐ„๋‹จํ•œ ์ถ”์ • (MB) + } + + private fun containsRandomFunction(expression: String): Boolean { + return Regex("\\b(random|rand)\\b", RegexOption.IGNORE_CASE).containsMatchIn(expression) + } + + private fun containsTimeFunction(expression: String): Boolean { + return Regex("\\b(now|time|date)\\b", RegexOption.IGNORE_CASE).containsMatchIn(expression) + } + + private fun getSessionMetrics(sessionId: String): SessionMetrics { + return sessionMetrics.getOrPut(sessionId) { SessionMetrics() } + } + + /** + * ์„ธ์…˜ ๋ฉ”ํŠธ๋ฆญ์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. + * + * @param sessionId ์„ธ์…˜ ID + * @param executionTime ์‹คํ–‰ ์‹œ๊ฐ„ + * @param memoryUsage ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ + */ + fun updateSessionMetrics(sessionId: String, executionTime: Long, memoryUsage: Int) { + val metrics = getSessionMetrics(sessionId) + metrics.updateMetrics(executionTime, memoryUsage) + } + + /** + * ์„ธ์…˜ ๋ฉ”ํŠธ๋ฆญ์„ ๋‚˜ํƒ€๋‚ด๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + private data class SessionMetrics( + var totalCalculations: Long = 0, + var totalExecutionTimeMs: Long = 0, + var totalMemoryUsageMB: Long = 0, + var concurrentCalculations: Int = 0, + var lastActivity: Long = System.currentTimeMillis() + ) { + val averageCalculationTimeMs: Long + get() = if (totalCalculations > 0) totalExecutionTimeMs / totalCalculations else 0 + + val memoryUsageMB: Int + get() = if (totalCalculations > 0) (totalMemoryUsageMB / totalCalculations).toInt() else 0 + + fun updateMetrics(executionTime: Long, memoryUsage: Int) { + totalCalculations++ + totalExecutionTimeMs += executionTime + totalMemoryUsageMB += memoryUsage + lastActivity = System.currentTimeMillis() + } + } + + /** + * ์†๋„ ์ œํ•œ์„ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + private data class RateLimiter( + private val maxRequests: Int, + private val windowMs: Long + ) { + private val requests = mutableListOf() + + fun isAllowed(): Boolean { + val now = System.currentTimeMillis() + + // ์œˆ๋„์šฐ ๋ฐ–์˜ ์š”์ฒญ๋“ค ์ œ๊ฑฐ + requests.removeAll { now - it > windowMs } + + return if (requests.size < maxRequests) { + requests.add(now) + true + } else { + false + } + } + } + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxExpressionLength" to MAX_EXPRESSION_LENGTH, + "maxCalculationTimeMs" to MAX_CALCULATION_TIME_MS, + "maxMemoryUsageMB" to MAX_MEMORY_USAGE_MB, + "maxRecursionDepth" to MAX_RECURSION_DEPTH, + "maxVariablesPerSession" to MAX_VARIABLES_PER_SESSION, + "maxSessionDurationHours" to MAX_SESSION_DURATION_HOURS, + "maxCalculationsPerMinute" to MAX_CALCULATIONS_PER_MINUTE, + "maxConcurrentCalculations" to MAX_CONCURRENT_CALCULATIONS, + "allowedPatterns" to ALLOWED_EXPRESSION_PATTERNS.size, + "forbiddenPatterns" to FORBIDDEN_PATTERNS.size + ) + + /** + * ์ •์ฑ…์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "policyName" to POLICY_NAME, + "activeSessions" to sessionMetrics.size, + "activeRateLimiters" to rateLimiters.size, + "securityRules" to SECURITY_RULES, + "performanceRules" to PERFORMANCE_RULES + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/services/CalculatorService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/services/CalculatorService.kt new file mode 100644 index 00000000..6566ac97 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/services/CalculatorService.kt @@ -0,0 +1,930 @@ +package hs.kr.entrydsm.domain.calculator.services + +import hs.kr.entrydsm.domain.calculator.entities.CalculationSession +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import hs.kr.entrydsm.domain.calculator.values.CalculationResult +import hs.kr.entrydsm.domain.calculator.policies.CalculationPolicy +import hs.kr.entrydsm.domain.calculator.specifications.CalculationValiditySpec +import hs.kr.entrydsm.domain.evaluator.aggregates.ExpressionEvaluator +import hs.kr.entrydsm.domain.lexer.aggregates.LexerAggregate +import hs.kr.entrydsm.domain.parser.aggregates.LRParser +import hs.kr.entrydsm.domain.ast.services.TreeOptimizer +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.global.configuration.CalculatorConfiguration +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider +import java.security.MessageDigest +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.* +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +/** + * ๊ณ„์‚ฐ๊ธฐ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋„๋ฉ”์ธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Domain Service ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๊ณ„์‚ฐ ์š”์ฒญ์˜ ์ฒ˜๋ฆฌ์™€ ๊ฒฐ๊ณผ ์ƒ์„ฑ์„ + * ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ๋ ‰์‹ฑ, ํŒŒ์‹ฑ, ํ‰๊ฐ€์˜ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ์„ ์กฐ์œจํ•˜๋ฉฐ + * ์ •์ฑ…๊ณผ ๋ช…์„ธ๋ฅผ ์ ์šฉํ•˜์—ฌ ์•ˆ์ „ํ•˜๊ณ  ์ •ํ™•ํ•œ ๊ณ„์‚ฐ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Service( + name = "CalculatorService", + type = hs.kr.entrydsm.global.annotation.service.type.ServiceType.DOMAIN_SERVICE +) +class CalculatorService( + private val lexer: LexerAggregate, + private val parser: LRParser, + private val evaluator: ExpressionEvaluator, + private val calculationPolicy: CalculationPolicy, + private val validitySpec: CalculationValiditySpec, + private val treeOptimizer: TreeOptimizer, + private val configurationProvider: ConfigurationProvider +) { + + companion object { + private const val CALCULATION_SERVICE = "CalculationService" + private const val ANONYMOUS = "anonymous" + private const val UNKNOWN_ERROR = "Unknown error" + private const val CALCULATOR_SERVICE = "CalculatorService" + } + + // ์„ค์ •์€ ConfigurationProvider๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ์ ‘๊ทผ + private val config: CalculatorConfiguration + get() = configurationProvider.getCalculatorConfiguration() + + private val calculationCache = ConcurrentHashMap() + private val performanceMetrics = PerformanceMetrics() + private val requestCounter = AtomicLong(0) + + // ์ฝ”๋ฃจํ‹ด ์Šค์ฝ”ํ”„ ๋ฐ ๋””์ŠคํŒจ์ฒ˜ ์„ค์ • + private val calculationScope = CoroutineScope( + Dispatchers.Default + SupervisorJob() + CoroutineName(CALCULATION_SERVICE) + ) + private val calculationDispatcher: CoroutineDispatcher + get() = Dispatchers.Default.limitedParallelism(config.concurrency) + + /** + * ๊ณ„์‚ฐ ์š”์ฒญ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ (์„ ํƒ์ ) + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + */ + fun calculate(request: CalculationRequest, session: CalculationSession? = null): CalculationResult { + val startTime = System.currentTimeMillis() + + try { + performanceMetrics.incrementTotalRequests() + + // ์ฃผ๊ธฐ์  ์บ์‹œ ์ •๋ฆฌ (100๋ฒˆ ์š”์ฒญ๋งˆ๋‹ค) + if (requestCounter.incrementAndGet() % 100 == 0L) { + manageCaches() + } + + // 1. ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + validateRequest(request, session) + + // 2. ์ •์ฑ… ๊ฒ€์ฆ + checkPolicy(request, session) + + // 3. ์บ์‹œ ํ™•์ธ + val cachedResult = retrieveFromCache(request, session) + if (cachedResult != null) { + return cachedResult + } + + // 4. ๊ณ„์‚ฐ ์‹คํ–‰ + val result = performCalculation(request, session) + + // 5. ๊ฒฐ๊ณผ ์บ์‹ฑ + cacheResultIfNeeded(request, session, result) + + // 6. ๋ฉ”ํŠธ๋ฆญ ์—…๋ฐ์ดํŠธ + val executionTime = System.currentTimeMillis() - startTime + updateMetrics(request, session, executionTime) + + return result + + } catch (e: DomainException) { + // ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋Š” ์ด๋ฏธ ์ ์ ˆํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ์Œ + performanceMetrics.incrementFailures() + return createFailureResult(request, e.message ?: "๋„๋ฉ”์ธ ์˜ค๋ฅ˜", startTime) + + } catch (e: IllegalArgumentException) { + // ์ž˜๋ชป๋œ ์ธ์ˆ˜ ์˜ˆ์™ธ + performanceMetrics.incrementFailures() + throw DomainException( + errorCode = ErrorCode.VALIDATION_FAILED, + message = "์ž˜๋ชป๋œ ๊ณ„์‚ฐ ์ธ์ˆ˜: ${e.message}", + cause = e, + context = mapOf( + "formula" to request.formula, + "sessionId" to (session?.sessionId ?: ANONYMOUS) + ) + ) + + } catch (e: ArithmeticException) { + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ ์˜ˆ์™ธ + performanceMetrics.incrementFailures() + throw DomainException( + errorCode = ErrorCode.MATH_ERROR, + message = "์ˆ˜ํ•™ ์—ฐ์‚ฐ ์˜ค๋ฅ˜: ${e.message}", + cause = e, + context = mapOf( + "formula" to request.formula, + "sessionId" to (session?.sessionId ?: ANONYMOUS) + ) + ) + + } catch (e: Exception) { + // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์‹œ์Šคํ…œ ์˜ˆ์™ธ + performanceMetrics.incrementFailures() + throw DomainException( + errorCode = ErrorCode.UNEXPECTED_ERROR, + message = "๊ณ„์‚ฐ ์‹คํ–‰ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜: ${e.message}", + cause = e, + context = mapOf( + "formula" to request.formula, + "sessionId" to (session?.sessionId ?: ANONYMOUS), + "exceptionType" to e.javaClass.simpleName + ) + ) + } + } + + /** + * ์ผ๊ด„ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param requests ๊ณ„์‚ฐ ์š”์ฒญ๋“ค + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋“ค + */ + fun calculateBatch(requests: List, session: CalculationSession? = null): List { + if (requests.isEmpty()) { + throw CalculatorException.requestListEmpty() + } + + return requests.map { request -> + calculate(request, session) + } + } + + /** + * ๋ณ‘๋ ฌ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * Kotlin ์ฝ”๋ฃจํ‹ด์„ ์‚ฌ์šฉํ•˜์—ฌ ํšจ์œจ์ ์ธ ๋น„๋™๊ธฐ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @param requests ๊ณ„์‚ฐ ์š”์ฒญ๋“ค + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ + * @param concurrency ๋™์‹œ ์‹คํ–‰ํ•  ์ตœ๋Œ€ ์ž‘์—… ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 10) + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋“ค + */ + fun calculateParallel( + requests: List, + session: CalculationSession? = null, + concurrency: Int = DEFAULT_CONCURRENCY + ): List { + if (requests.isEmpty()) { + throw CalculatorException.requestListEmpty() + } + + if (concurrency <= 0) { + throw CalculatorException.invalidConcurrencyLevel(concurrency) + } + + return runBlocking(calculationDispatcher.limitedParallelism(concurrency)) { + requests.map { request -> + async { + try { + calculate(request, session) + } catch (e: Exception) { + // ๊ฐœ๋ณ„ ๊ณ„์‚ฐ ์‹คํŒจ๊ฐ€ ์ „์ฒด ๋ฐฐ์น˜๋ฅผ ์ค‘๋‹จ์‹œํ‚ค์ง€ ์•Š๋„๋ก ์ฒ˜๋ฆฌ + createFailureResult(request, "๋ณ‘๋ ฌ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: ${e.message}", System.currentTimeMillis()) + } + } + }.awaitAll() + } + } + + /** + * Flow ๊ธฐ๋ฐ˜ ์ŠคํŠธ๋ฆฌ๋ฐ ๋ณ‘๋ ฌ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * ๋Œ€์šฉ๋Ÿ‰ ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ์— ์ ํ•ฉํ•˜๋ฉฐ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ด๊ณ  ๋ฐฑํ”„๋ ˆ์…”๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * @param requests ๊ณ„์‚ฐ ์š”์ฒญ๋“ค + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ + * @param concurrency ๋™์‹œ ์‹คํ–‰ํ•  ์ตœ๋Œ€ ์ž‘์—… ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 10) + * @param bufferSize ๋ฒ„ํผ ํฌ๊ธฐ (๊ธฐ๋ณธ๊ฐ’: 50) + * @return ๊ณ„์‚ฐ ๊ฒฐ๊ณผ Flow + */ + fun calculateParallelFlow( + requests: List, + session: CalculationSession? = null, + concurrency: Int = DEFAULT_CONCURRENCY, + bufferSize: Int = 50 + ): Flow { + if (requests.isEmpty()) { + throw CalculatorException.requestListEmpty() + } + + if (concurrency <= 0) { + throw CalculatorException.invalidConcurrencyLevel(concurrency) + } + + if (bufferSize <= 0) { + throw CalculatorException.invalidBufferSize(bufferSize) + } + + return requests.asFlow() + .buffer(capacity = bufferSize) + .map { request -> + try { + calculate(request, session) + } catch (e: Exception) { + createFailureResult(request, "์ŠคํŠธ๋ฆฌ๋ฐ ๊ณ„์‚ฐ ์˜ค๋ฅ˜: ${e.message}", System.currentTimeMillis()) + } + } + .flowOn(calculationDispatcher.limitedParallelism(concurrency)) + } + + /** + * ํ‘œํ˜„์‹์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๊ฒ€์ฆํ•  ํ‘œํ˜„์‹ + * @param variables ๋ณ€์ˆ˜๋“ค + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateExpression(expression: String, variables: Map = emptyMap()): Boolean { + val request = CalculationRequest( + formula = expression, + variables = variables + ) + return validitySpec.isSatisfiedBy(request) + } + + /** + * ํ‘œํ˜„์‹์„ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๋ถ„์„ํ•  ํ‘œํ˜„์‹ + * @return ๋ถ„์„ ๊ฒฐ๊ณผ + */ + fun analyzeExpression(expression: String): Map { + try { + val lexingResult = lexer.tokenize(expression) + val tokens = if (lexingResult.isSuccess) lexingResult.tokens else emptyList() + val ast = parser.parse(tokens) + + return mapOf( + "tokenCount" to tokens.size, + "parseTree" to ast.toString(), + "variables" to extractVariables(expression), + "functions" to extractFunctions(expression), + "complexity" to calculationPolicy.calculateTimeout(expression) / 1000, + "isValid" to validateExpression(expression), + "riskScore" to validitySpec.calculateRiskScore(expression) + ) + } catch (e: Exception) { + return mapOf( + "error" to (e.message ?: UNKNOWN_ERROR), + "isValid" to false + ) + } + } + + /** + * ๊ณ„์‚ฐ ์„ฑ๋Šฅ์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ์ตœ์ ํ™”ํ•  ํ‘œํ˜„์‹ + * @return ์ตœ์ ํ™”๋œ ํ‘œํ˜„์‹ + */ + fun optimizeExpression(expression: String): String { + // ๊ฐ„๋‹จํ•œ ์ตœ์ ํ™” ์˜ˆ์‹œ๋“ค + var optimized = expression.trim() + + // ์ค‘๋ณต ๊ณต๋ฐฑ ์ œ๊ฑฐ + optimized = optimized.replace(Regex("\\s+"), " ") + + // ๋ถˆํ•„์š”ํ•œ ๊ด„ํ˜ธ ์ œ๊ฑฐ (๊ฐ„๋‹จํ•œ ๊ฒฝ์šฐ๋งŒ) + optimized = optimized.replace(Regex("\\(\\s*(\\d+(?:\\.\\d+)?)\\s*\\)"), "$1") + + // ์ƒ์ˆ˜ ํด๋”ฉ์€ AST ์ตœ์ ํ™” ๋‹จ๊ณ„์—์„œ TreeOptimizer์— ์œ„์ž„๋จ + // ๋ฌธ์ž์—ด ๊ธฐ๋ฐ˜ ์ตœ์ ํ™” ๋Œ€์‹  AST ๊ธฐ๋ฐ˜ ์ƒ์ˆ˜ ํด๋”ฉ ์‚ฌ์šฉ + + return optimized + } + + /** + * ์บ์‹œ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * ConcurrentHashMap์„ ์‚ฌ์šฉํ•˜์—ฌ ์Šค๋ ˆ๋“œ ์•ˆ์ „์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxSize ์ตœ๋Œ€ ์บ์‹œ ํฌ๊ธฐ + * @param maxAge ์ตœ๋Œ€ ์บ์‹œ ์œ ์ง€ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun manageCaches(maxSize: Int = 1000, maxAge: Long = 3600000) { + val currentTime = System.currentTimeMillis() + + val expiredKeys = calculationCache.entries + .filter { (_, cached) -> currentTime - cached.timestamp > maxAge } + .map { it.key } + + expiredKeys.forEach { key -> + calculationCache.remove(key) + } + + if (calculationCache.size > maxSize) { + val entriesToRemove = calculationCache.entries + .sortedBy { it.value.timestamp } + .take(calculationCache.size - maxSize) + .map { it.key } + + entriesToRemove.forEach { key -> + calculationCache.remove(key) + } + } + } + + private suspend fun executeCalculation(request: CalculationRequest, session: CalculationSession?): CalculationResult { + val startTime = System.currentTimeMillis() + + try { + // 1. ๋ ‰์‹ฑ + val lexingResult = lexer.tokenize(request.formula) + val tokens = if (lexingResult.isSuccess) lexingResult.tokens else emptyList() + + // 2. ํŒŒ์‹ฑ + val ast = parser.parse(tokens) + + // 3. AST ์ตœ์ ํ™” (์ƒ์ˆ˜ ํด๋”ฉ ํฌํ•จ) + val optimizedAst = treeOptimizer.optimize(ast) + + // 4. ๋ณ€์ˆ˜ ๊ฒฐํ•ฉ + val allVariables = mutableMapOf() + session?.variables?.let { allVariables.putAll(it) } + allVariables.putAll(request.variables) + + // 5. ํ‰๊ฐ€ (์ตœ์ ํ™”๋œ AST ์‚ฌ์šฉ) + val evaluationResult = evaluateWithRetry(optimizedAst, allVariables) + + val executionTime = System.currentTimeMillis() - startTime + + return CalculationResult( + result = evaluationResult, + executionTimeMs = executionTime, + formula = request.formula, + metadata = mapOf( + "tokenCount" to tokens.size, + "variableCount" to allVariables.size, + "astDepth" to calculateASTDepth(optimizedAst), + "optimized" to true + ) + ) + + } catch (e: Exception) { + return createFailureResult(request, "๊ณ„์‚ฐ ์˜ค๋ฅ˜: ${e.message}", startTime) + } + } + + private suspend fun evaluateWithRetry(ast: Any, variables: Map, retries: Int = config.maxRetries): Any? { + repeat(retries) { attempt -> + try { + // AST๋ฅผ ์‹ค์ œ ASTNode๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ํ‰๊ฐ€ + return evaluateAST(ast, variables) + } catch (e: Exception) { + if (attempt == retries - 1) { + throw e + } + delay(100) + } + } + throw CalculatorException.maxRetryExceeded() + } + + private fun evaluateAST(ast: Any, variables: Map): Any? { + return try { + val astNode = ast as? hs.kr.entrydsm.domain.ast.entities.ASTNode + ?: throw CalculatorException.invalidAstNodeType(ast.javaClass.simpleName) + + val evaluatorWithVariables = if (variables.isNotEmpty()) { + evaluator.withVariables(variables) + } else { + evaluator + } + + val result = evaluatorWithVariables.evaluate(astNode) + + validateEvaluationResult(result, astNode) + + result + + } catch (e: IllegalArgumentException) { + throw DomainException( + errorCode = ErrorCode.VALIDATION_FAILED, + message = "AST ํ‰๊ฐ€ ์‹คํŒจ: ${e.message}", + cause = e, + context = mapOf( + "astType" to ast.javaClass.simpleName, + "variableCount" to variables.size, + "variables" to variables.keys.joinToString(", ") + ) + ) + } catch (e: ArithmeticException) { + throw DomainException( + errorCode = ErrorCode.MATH_ERROR, + message = "์ˆ˜ํ•™ ์—ฐ์‚ฐ ์˜ค๋ฅ˜: ${e.message}", + cause = e, + context = mapOf( + "astType" to ast.javaClass.simpleName, + "variableCount" to variables.size, + "variables" to variables.keys.joinToString(", ") + ) + ) + } catch (e: Exception) { + throw DomainException( + errorCode = ErrorCode.UNEXPECTED_ERROR, + message = "AST ํ‰๊ฐ€ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜: ${e.message}", + cause = e, + context = mapOf( + "astType" to ast.javaClass.simpleName, + "variableCount" to variables.size, + "variables" to variables.keys.joinToString(", "), + "exceptionType" to e.javaClass.simpleName + ) + ) + } + } + + /** + * ํ‰๊ฐ€ ๊ฒฐ๊ณผ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateEvaluationResult(result: Any?, astNode: hs.kr.entrydsm.domain.ast.entities.ASTNode) { + when (result) { + is Double -> { + if (result.isNaN()) { + throw DomainException( + errorCode = ErrorCode.MATH_ERROR, + message = "๊ณ„์‚ฐ ๊ฒฐ๊ณผ๊ฐ€ NaN์ž…๋‹ˆ๋‹ค", + context = mapOf( + "astType" to astNode.javaClass.simpleName, + "result" to "NaN" + ) + ) + } + if (result.isInfinite()) { + throw DomainException( + errorCode = ErrorCode.MATH_ERROR, + message = "๊ณ„์‚ฐ ๊ฒฐ๊ณผ๊ฐ€ ๋ฌดํ•œ๋Œ€์ž…๋‹ˆ๋‹ค", + context = mapOf( + "astType" to astNode.javaClass.simpleName, + "result" to if (result > 0) "+Infinity" else "-Infinity" + ) + ) + } + } + is Float -> { + if (result.isNaN()) { + throw DomainException( + errorCode = ErrorCode.MATH_ERROR, + message = "๊ณ„์‚ฐ ๊ฒฐ๊ณผ๊ฐ€ NaN์ž…๋‹ˆ๋‹ค", + context = mapOf( + "astType" to astNode.javaClass.simpleName, + "result" to "NaN" + ) + ) + } + if (result.isInfinite()) { + throw DomainException( + errorCode = ErrorCode.MATH_ERROR, + message = "๊ณ„์‚ฐ ๊ฒฐ๊ณผ๊ฐ€ ๋ฌดํ•œ๋Œ€์ž…๋‹ˆ๋‹ค", + context = mapOf( + "astType" to astNode.javaClass.simpleName, + "result" to if (result > 0) "+Infinity" else "-Infinity" + ) + ) + } + } + } + } + + private fun createFailureResult(request: CalculationRequest, error: String, startTime: Long): CalculationResult { + val executionTime = System.currentTimeMillis() - startTime + return CalculationResult( + result = null, + executionTimeMs = executionTime, + formula = request.formula, + errors = listOf(error) + ) + } + + private fun generateCacheKey(request: CalculationRequest, session: CalculationSession?): String { + val variables = (session?.variables ?: emptyMap()) + request.variables + + // ๋ณ€์ˆ˜๋“ค์„ ํ‚ค๋กœ ์ •๋ ฌํ•˜์—ฌ ์ผ๊ด€๋œ ๋ฌธ์ž์—ด ์ƒ์„ฑ + val sortedVariables = variables.entries.sortedBy { it.key } + .joinToString(",") { "${it.key}=${it.value}" } + + // ์ˆ˜์‹๊ณผ ๋ณ€์ˆ˜๋ฅผ ๊ฒฐํ•ฉํ•œ ๋ฌธ์ž์—ด + val combinedString = "${request.formula}|$sortedVariables" + + // SHA-256 ํ•ด์‹œ๋กœ ์•ˆ์ „ํ•œ ์บ์‹œ ํ‚ค ์ƒ์„ฑ + return try { + val digest = MessageDigest.getInstance("SHA-256") + val hashBytes = digest.digest(combinedString.toByteArray()) + hashBytes.joinToString("") { "%02x".format(it) } + } catch (e: Exception) { + // SHA-256์„ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ fallback์œผ๋กœ ์•ˆ์ „ํ•œ ๋ฌธ์ž์—ด ๊ธฐ๋ฐ˜ ํ‚ค ์‚ฌ์šฉ + "formula_${request.formula.replace("[^a-zA-Z0-9]".toRegex(), "_")}_vars_${sortedVariables.replace("[^a-zA-Z0-9,=]".toRegex(), "_")}" + .take(200) // ํ‚ค ๊ธธ์ด ์ œํ•œ + } + } + + private fun getCachedResult(key: String): CachedResult? { + val cached = calculationCache[key] + return if (cached != null && System.currentTimeMillis() - cached.timestamp < 3600000) { + cached + } else { + if (cached != null) { + calculationCache.remove(key) + } + null + } + } + + private fun cacheResult(key: String, result: CalculationResult) { + if (result.isSuccess()) { + + if (calculationCache.size >= 950) { + manageCaches(900) + } + + if (calculationCache.size < 1000) { + calculationCache[key] = CachedResult( + result = result.result, + executionTime = result.executionTimeMs, + timestamp = System.currentTimeMillis() + ) + } + } + } + + private fun extractVariables(expression: String): Set { + val pattern = Regex("\\b[a-zA-Z_][a-zA-Z0-9_]*\\b") + return pattern.findAll(expression) + .map { it.value } + .filter { it.uppercase() !in setOf("PI", "E", "TRUE", "FALSE", "SIN", "COS", "TAN", "LOG", "ABS") } + .toSet() + } + + private fun extractFunctions(expression: String): Set { + val pattern = Regex("([a-zA-Z_][a-zA-Z0-9_]*)\\s*\\(") + return pattern.findAll(expression) + .map { it.groupValues[1] } + .toSet() + } + + + /** + * AST์˜ ์‹ค์ œ ๊ตฌ์กฐ์  ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•  AST ๊ฐ์ฒด + * @return AST์˜ ์ตœ๋Œ€ ๊นŠ์ด (๋ฃจํŠธ์—์„œ ๊ฐ€์žฅ ๊นŠ์€ ๋ฆฌํ”„ ๋…ธ๋“œ๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ) + */ + private fun calculateASTDepth(ast: Any): Int { + return try { + val astNode = ast as? hs.kr.entrydsm.domain.ast.entities.ASTNode + ?: throw CalculatorException.invalidAstNodeType(ast.javaClass.simpleName) + + calculateNodeDepth(astNode) + + } catch (e: IllegalArgumentException) { + throw DomainException( + errorCode = ErrorCode.VALIDATION_FAILED, + message = "AST ๊นŠ์ด ๊ณ„์‚ฐ ์‹คํŒจ: ${e.message}", + cause = e, + context = mapOf( + "astType" to ast.javaClass.simpleName, + "operation" to "calculateASTDepth" + ) + ) + } catch (e: StackOverflowError) { + throw DomainException( + errorCode = ErrorCode.AST_DEPTH_EXCEEDED, + message = "AST ๊นŠ์ด ๊ณ„์‚ฐ ์ค‘ ์Šคํƒ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ: AST๊ฐ€ ๋„ˆ๋ฌด ๊นŠ์Šต๋‹ˆ๋‹ค", + cause = e, + context = mapOf( + "astType" to ast.javaClass.simpleName, + "operation" to "calculateASTDepth", + "error" to "StackOverflow" + ) + ) + } catch (e: Exception) { + throw DomainException( + errorCode = ErrorCode.UNEXPECTED_ERROR, + message = "AST ๊นŠ์ด ๊ณ„์‚ฐ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜: ${e.message}", + cause = e, + context = mapOf( + "astType" to ast.javaClass.simpleName, + "operation" to "calculateASTDepth", + "exceptionType" to e.javaClass.simpleName + ) + ) + } + } + + /** + * AST ๋…ธ๋“œ์˜ ์‹ค์ œ ๊นŠ์ด๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•  AST ๋…ธ๋“œ + * @return ํ•ด๋‹น ๋…ธ๋“œ๋ฅผ ๋ฃจํŠธ๋กœ ํ•˜๋Š” ์„œ๋ธŒํŠธ๋ฆฌ์˜ ์ตœ๋Œ€ ๊นŠ์ด + */ + private fun calculateNodeDepth(node: hs.kr.entrydsm.domain.ast.entities.ASTNode): Int { + return when (node) { + // ๋ฆฌํ”„ ๋…ธ๋“œ๋“ค์€ ๊นŠ์ด 1 + is hs.kr.entrydsm.domain.ast.entities.NumberNode, + is hs.kr.entrydsm.domain.ast.entities.BooleanNode, + is hs.kr.entrydsm.domain.ast.entities.VariableNode -> 1 + + // ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋…ธ๋“œ: 1 + ํ”ผ์—ฐ์‚ฐ์ž์˜ ๊นŠ์ด + is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode -> { + 1 + calculateNodeDepth(node.operand) + } + + // ์ดํ•ญ ์—ฐ์‚ฐ์ž ๋…ธ๋“œ: 1 + max(์™ผ์ชฝ ์ž์‹, ์˜ค๋ฅธ์ชฝ ์ž์‹) + is hs.kr.entrydsm.domain.ast.entities.BinaryOpNode -> { + 1 + maxOf( + calculateNodeDepth(node.left), + calculateNodeDepth(node.right) + ) + } + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ: 1 + ์ธ์ˆ˜๋“ค ์ค‘ ์ตœ๋Œ€ ๊นŠ์ด + is hs.kr.entrydsm.domain.ast.entities.FunctionCallNode -> { + if (node.args.isEmpty()) { + 1 + } else { + 1 + node.args.maxOf { calculateNodeDepth(it) } + } + } + + // ์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ (IF): 1 + max(์กฐ๊ฑด, ์ฐธ๊ฐ’, ๊ฑฐ์ง“๊ฐ’) + is hs.kr.entrydsm.domain.ast.entities.IfNode -> { + 1 + maxOf( + calculateNodeDepth(node.condition), + calculateNodeDepth(node.trueValue), + calculateNodeDepth(node.falseValue) + ) + } + + // ์ธ์ˆ˜ ๋ชฉ๋ก ๋…ธ๋“œ: 1 + ์ธ์ˆ˜๋“ค ์ค‘ ์ตœ๋Œ€ ๊นŠ์ด + is hs.kr.entrydsm.domain.ast.entities.ArgumentsNode -> { + if (node.arguments.isEmpty()) { + 1 + } else { + 1 + node.arguments.maxOf { calculateNodeDepth(it) } + } + } + + // ์•Œ ์ˆ˜ ์—†๋Š” ๋…ธ๋“œ ํƒ€์ž…: getChildren() ๋ฉ”์„œ๋“œ ํ™œ์šฉ + else -> { + try { + val children = node.getChildren() + if (children.isEmpty()) { + 1 // ์ž์‹์ด ์—†์œผ๋ฉด ๋ฆฌํ”„ ๋…ธ๋“œ + } else { + 1 + children.maxOf { calculateNodeDepth(it) } + } + } catch (e: Exception) { + throw DomainException( + errorCode = ErrorCode.AST_TRAVERSAL_ERROR, + message = "์•Œ ์ˆ˜ ์—†๋Š” AST ๋…ธ๋“œ ํƒ€์ž…์˜ ๊นŠ์ด ๊ณ„์‚ฐ ์‹คํŒจ: ${e.message}", + cause = e, + context = mapOf( + "nodeType" to node.javaClass.simpleName, + "operation" to "calculateNodeDepth", + "exceptionType" to e.javaClass.simpleName + ) + ) + } + } + } + } + + private fun estimateMemoryUsage(expression: String): Int { + // ๊ฐ„๋‹จํ•œ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ถ”์ • + return (expression.length * 0.001).toInt() + 1 // MB + } + + /** + * ์บ์‹œ๋œ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + private data class CachedResult( + val result: Any?, + val executionTime: Long, + val timestamp: Long + ) { + fun toCalculationResult(formula: String, startTime: Long): CalculationResult { + return CalculationResult( + result = result, + executionTimeMs = System.currentTimeMillis() - startTime, + formula = formula, + metadata = mapOf("fromCache" to true) + ) + } + } + + /** + * ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ์„ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + private class PerformanceMetrics { + private var totalRequests = 0L + private var totalFailures = 0L + private var totalCacheHits = 0L + private var totalExecutionTime = 0L + private var requestCount = 0L + + fun incrementTotalRequests() = synchronized(this) { totalRequests++ } + fun incrementFailures() = synchronized(this) { totalFailures++ } + fun incrementCacheHits() = synchronized(this) { totalCacheHits++ } + + fun updateExecutionTime(time: Long) = synchronized(this) { + totalExecutionTime += time + requestCount++ + } + + fun getMetrics(): Map = synchronized(this) { + mapOf( + "totalRequests" to totalRequests, + "totalFailures" to totalFailures, + "totalCacheHits" to totalCacheHits, + "averageExecutionTime" to if (requestCount > 0) totalExecutionTime.toDouble() / requestCount else 0.0, + "successRate" to if (totalRequests > 0) ((totalRequests - totalFailures).toDouble() / totalRequests) else 0.0, + "cacheHitRate" to if (totalRequests > 0) (totalCacheHits.toDouble() / totalRequests) else 0.0 + ) + } + } + + /** + * ์„œ๋น„์Šค์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "serviceName" to CALCULATOR_SERVICE, + "defaultTimeoutMs" to config.defaultTimeoutMs, + "maxRetries" to config.maxRetries, + "cacheEnabled" to true, + "maxCacheSize" to 1000, + "cacheExpirationMs" to 3600000, + "parallelProcessingEnabled" to true + ) + + /** + * ์„œ๋น„์Šค์˜ ์„ฑ๋Šฅ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๋Šฅ ํ†ต๊ณ„ ๋งต + */ + fun getStatistics(): Map { + val metrics = performanceMetrics.getMetrics() + return metrics + mapOf( + "currentCacheSize" to calculationCache.size, + "maxCacheSize" to 1000 + ) + } + + /** + * ์„œ๋น„์Šค ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒํƒœ ์ •๋ณด ๋งต + */ + fun getStatus(): Map = mapOf( + "status" to "active", + "healthCheck" to checkHealth(), + "lastActivity" to System.currentTimeMillis(), + "cacheStatus" to mapOf( + "size" to calculationCache.size, + "enabled" to true + ) + ) + + /** + * ์š”์ฒญ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateRequest(request: CalculationRequest, session: CalculationSession?) { + if (!validitySpec.isSatisfiedBy(request, session)) { + val errors = validitySpec.getValidationErrors(request, session) + throw DomainException( + errorCode = ErrorCode.VALIDATION_FAILED, + message = "์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ: ${errors.joinToString(", ") { it.message }}", + context = mapOf( + "formula" to request.formula, + "errorCount" to errors.size, + "errors" to errors.map { it.message } + ) + ) + } + } + + /** + * ๊ณ„์‚ฐ ์ •์ฑ…์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun checkPolicy(request: CalculationRequest, session: CalculationSession?) { + if (session != null && !calculationPolicy.isCalculationAllowed(request, session)) { + throw DomainException( + errorCode = ErrorCode.BUSINESS_RULE_VIOLATION, + message = "๊ณ„์‚ฐ ์ •์ฑ… ์œ„๋ฐ˜", + context = mapOf( + "formula" to request.formula, + "sessionId" to session.sessionId + ) + ) + } + } + + /** + * ์บ์‹œ์—์„œ ๊ฒฐ๊ณผ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + private fun retrieveFromCache(request: CalculationRequest, session: CalculationSession?): CalculationResult? { + if (session?.settings?.enableCaching != true) return null + + val cacheKey = generateCacheKey(request, session) + val cachedResult = getCachedResult(cacheKey) + + return if (cachedResult != null) { + performanceMetrics.incrementCacheHits() + cachedResult.toCalculationResult(request.formula, System.currentTimeMillis()) + } else null + } + + /** + * ์‹ค์ œ ๊ณ„์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performCalculation(request: CalculationRequest, session: CalculationSession?): CalculationResult { + return runBlocking { + executeCalculation(request, session) + } + } + + /** + * ํ•„์š”ํ•œ ๊ฒฝ์šฐ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + */ + private fun cacheResultIfNeeded(request: CalculationRequest, session: CalculationSession?, result: CalculationResult) { + if (session?.settings?.enableCaching == true && result.isSuccess()) { + val cacheKey = generateCacheKey(request, session) + cacheResult(cacheKey, result) + } + } + + /** + * ๋ฉ”ํŠธ๋ฆญ์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun updateMetrics(request: CalculationRequest, session: CalculationSession?, executionTime: Long) { + calculationPolicy.updateSessionMetrics( + session?.sessionId ?: ANONYMOUS, + executionTime, + estimateMemoryUsage(request.formula) + ) + performanceMetrics.updateExecutionTime(executionTime) + } + + /** + * ์„œ๋น„์Šค ๊ฑด๊ฐ• ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฑด๊ฐ•ํ•˜๋ฉด true + */ + private fun checkHealth(): Boolean { + return try { + // ๊ฐ„๋‹จํ•œ ๊ณ„์‚ฐ์œผ๋กœ ๊ฑด๊ฐ• ์ƒํƒœ ํ™•์ธ + val testRequest = CalculationRequest("1+1", emptyMap()) + val result = calculate(testRequest) + result.isSuccess() + } catch (e: DomainException) { + // ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋Š” ์ด๋ฏธ ์ ์ ˆํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ํฌํ•จํ•˜๊ณ  ์žˆ์Œ + throw DomainException( + errorCode = ErrorCode.HEALTH_CHECK_FAILED, + message = "์„œ๋น„์Šค ๊ฑด๊ฐ• ์ƒํƒœ ํ™•์ธ ์‹คํŒจ: ${e.message}", + cause = e, + context = mapOf( + "healthCheckFormula" to "1+1", + "originalErrorCode" to e.getCode(), + "originalDomain" to e.getDomain(), + "healthCheckFailed" to true + ) + ) + } catch (e: Exception) { + // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์‹œ์Šคํ…œ ์˜ˆ์™ธ + throw DomainException( + errorCode = ErrorCode.HEALTH_CHECK_FAILED, + message = "์„œ๋น„์Šค ๊ฑด๊ฐ• ์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜: ${e.message}", + cause = e, + context = mapOf( + "healthCheckFormula" to "1+1", + "exceptionType" to e.javaClass.simpleName, + "healthCheckFailed" to true + ) + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/services/ValidationService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/services/ValidationService.kt new file mode 100644 index 00000000..84c9125f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/services/ValidationService.kt @@ -0,0 +1,405 @@ +package hs.kr.entrydsm.domain.calculator.services + +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import hs.kr.entrydsm.domain.calculator.values.MultiStepCalculationRequest +import hs.kr.entrydsm.domain.calculator.values.CalculationStep +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.global.exception.ValidationException +import hs.kr.entrydsm.global.configuration.CalculatorConfiguration +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider + +/** + * ๊ณ„์‚ฐ๊ธฐ ๋„๋ฉ”์ธ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ๋„๋ฉ”์ธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * ์ˆ˜์‹ ๋ฐ ์š”์ฒญ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•˜๋Š” ์ฑ…์ž„์„ ๊ฐ€์ง€๋ฉฐ, ์ˆ˜์‹ ๊ธธ์ด, ๋‹จ๊ณ„ ์ˆ˜, + * ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ ๋“ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. POC ์ฝ”๋“œ์˜ FormulaValidator๋ฅผ DDD ์›์น™์— + * ๋งž๊ฒŒ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.21 + */ +@Service( + name = "ValidationService", + type = hs.kr.entrydsm.global.annotation.service.type.ServiceType.DOMAIN_SERVICE +) +class ValidationService( + private val configurationProvider: ConfigurationProvider +) { + + // ์„ค์ •์€ ConfigurationProvider๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ์ ‘๊ทผ + private val config: CalculatorConfiguration + get() = configurationProvider.getCalculatorConfiguration() + + /** + * ๋‹จ์ผ ๊ณ„์‚ฐ ์š”์ฒญ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @param maxFormulaLength ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์ˆ˜์‹ ๊ธธ์ด + * @param maxVariables ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @throws ValidationException ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ + */ + fun validateCalculationRequest( + request: CalculationRequest, + maxFormulaLength: Int = config.maxFormulaLength, + maxVariables: Int = config.maxVariables + ) { + validateFormula(request.formula, maxFormulaLength) + request.variables?.let { validateVariableCount(it, maxVariables) } + } + + /** + * ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ์š”์ฒญ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ์š”์ฒญ + * @param maxFormulaLength ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์ˆ˜์‹ ๊ธธ์ด + * @param maxSteps ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ๋‹จ๊ณ„ ์ˆ˜ + * @param maxVariables ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @throws ValidationException ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ + */ + fun validateMultiStepRequest( + request: MultiStepCalculationRequest, + maxFormulaLength: Int = config.maxFormulaLength, + maxSteps: Int = 50, // MultiStep ์ „์šฉ ์„ค์ • (์ถ”ํ›„ Configuration์— ์ถ”๊ฐ€ ๊ฐ€๋Šฅ) + maxVariables: Int = config.maxVariables + ) { + // ๋‹จ๊ณ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + if (request.steps.isNullOrEmpty()) { + throw ValidationException( + errorCode = ErrorCode.EMPTY_STEPS, + field = "steps", + value = request.steps, + constraint = "๋‹จ๊ณ„๋Š” ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + } + + if (request.steps.size > maxSteps) { + throw ValidationException( + errorCode = ErrorCode.TOO_MANY_STEPS, + field = "steps", + value = request.steps.size, + constraint = "์ตœ๋Œ€ ${maxSteps}๋‹จ๊ณ„๊นŒ์ง€ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค" + ) + } + + // ์ดˆ๊ธฐ ๋ณ€์ˆ˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + request.variables?.let { validateVariableCount(it, maxVariables) } + + // ๊ฐ ๋‹จ๊ณ„๋ณ„ ์ˆ˜์‹ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + request.steps.forEachIndexed { index, step -> + validateCalculationStep(step, index + 1, maxFormulaLength) + } + } + + /** + * ๊ณ„์‚ฐ ๋‹จ๊ณ„์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param step ๊ณ„์‚ฐ ๋‹จ๊ณ„ + * @param stepNumber ๋‹จ๊ณ„ ๋ฒˆํ˜ธ + * @param maxFormulaLength ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์ˆ˜์‹ ๊ธธ์ด + * @throws ValidationException ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ + */ + fun validateCalculationStep( + step: CalculationStep, + stepNumber: Int, + maxFormulaLength: Int = config.maxFormulaLength + ) { + validateFormula(step.formula, maxFormulaLength, "๋‹จ๊ณ„ $stepNumber") + + // ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช… ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + step.resultVariable?.let { resultVar -> + if (resultVar.isBlank()) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "resultVariable", + value = resultVar, + constraint = "๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช…์€ ๊ณต๋ฐฑ์ผ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + } + + if (!isValidVariableName(resultVar)) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "resultVariable", + value = resultVar, + constraint = "๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช…์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + ) + } + } + } + + /** + * ์ˆ˜์‹์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ๊ฒ€์‚ฌํ•  ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @param maxLength ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ๊ธธ์ด + * @param context ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€์— ์‚ฌ์šฉ๋  ์ปจํ…์ŠคํŠธ + * @throws ValidationException ์ˆ˜์‹์ด ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ๊ธด ๊ฒฝ์šฐ + */ + fun validateFormula( + formula: String, + maxLength: Int = config.maxFormulaLength, + context: String = "์ˆ˜์‹" + ) { + if (formula.isBlank()) { + throw ValidationException( + errorCode = ErrorCode.EMPTY_FORMULA, + field = "formula", + value = formula, + constraint = "${context}์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + } + + if (formula.length > maxLength) { + throw ValidationException( + errorCode = ErrorCode.FORMULA_TOO_LONG, + field = "formula", + value = formula.length, + constraint = "${context}์€ ์ตœ๋Œ€ ${maxLength}์ž๊นŒ์ง€ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค" + ) + } + } + + /** + * ํ•„์š”ํ•œ ๋ณ€์ˆ˜์™€ ์ œ๊ณต๋œ ๋ณ€์ˆ˜๋ฅผ ๋น„๊ตํ•˜์—ฌ ๋ˆ„๋ฝ๋œ ๋ณ€์ˆ˜๊ฐ€ ์žˆ๋Š”์ง€ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param requiredVars ์ˆ˜์‹์—์„œ ํ•„์š”ํ•œ ๋ณ€์ˆ˜ ์ง‘ํ•ฉ + * @param providedVars ์‚ฌ์šฉ์ž๋กœ๋ถ€ํ„ฐ ์ œ๊ณต๋œ ๋ณ€์ˆ˜ ๋งต + * @throws ValidationException ํ•„์ˆ˜ ๋ณ€์ˆ˜๊ฐ€ ๋ˆ„๋ฝ๋œ ๊ฒฝ์šฐ + */ + fun validateVariables( + requiredVars: Set, + providedVars: Map + ) { + val missingVars = requiredVars - providedVars.keys + + if (missingVars.isNotEmpty()) { + throw ValidationException( + errorCode = ErrorCode.MISSING_VARIABLES, + field = "variables", + value = missingVars, + constraint = "ํ•„์ˆ˜ ๋ณ€์ˆ˜๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค: ${missingVars.joinToString(", ")}" + ) + } + + // ๋ณ€์ˆ˜๊ฐ’ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + providedVars.forEach { (name, value) -> + validateVariableValue(name, value) + } + } + + /** + * ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param variables ๊ฒ€์‚ฌํ•  ๋ณ€์ˆ˜ ๋งต + * @param maxVariables ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + * @throws ValidationException ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์€ ๊ฒฝ์šฐ + */ + fun validateVariableCount( + variables: Map, + maxVariables: Int = config.maxVariables + ) { + if (variables.size > maxVariables) { + throw ValidationException( + errorCode = ErrorCode.TOO_MANY_VARIABLES, + field = "variables", + value = variables.size, + constraint = "๋ณ€์ˆ˜๋Š” ์ตœ๋Œ€ ${maxVariables}๊ฐœ๊นŒ์ง€ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค" + ) + } + } + + /** + * ๋ณ€์ˆ˜๋ช…์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param variableName ๊ฒ€์‚ฌํ•  ๋ณ€์ˆ˜๋ช… + * @return ์œ ํšจํ•œ ๋ณ€์ˆ˜๋ช…์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValidVariableName(variableName: String): Boolean { + if (variableName.isBlank()) return false + + // ๋ณ€์ˆ˜๋ช…์€ ์•ŒํŒŒ๋ฒณ, ์ˆซ์ž, ์–ธ๋”์Šค์ฝ”์–ด๋งŒ ํ—ˆ์šฉํ•˜๊ณ  ์ˆซ์ž๋กœ ์‹œ์ž‘ํ•  ์ˆ˜ ์—†์Œ + val validPattern = Regex("^[a-zA-Z_][a-zA-Z0-9_]*$") + return variableName.matches(validPattern) + } + + /** + * ๋ณ€์ˆ˜๊ฐ’์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param variableName ๋ณ€์ˆ˜๋ช… + * @param value ๋ณ€์ˆ˜๊ฐ’ + * @throws ValidationException ๋ณ€์ˆ˜๊ฐ’์ด ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateVariableValue(variableName: String, value: Any?) { + // null ๊ฐ’ ํ—ˆ์šฉ + if (value == null) return + + // ์ง€์›๋˜๋Š” ํƒ€์ž…: Number, String, Boolean + when (value) { + is Number -> { + // ๋ฌดํ•œ๋Œ€๋‚˜ NaN ์ฒดํฌ + val doubleValue = value.toDouble() + if (doubleValue.isInfinite() || doubleValue.isNaN()) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "variables.$variableName", + value = value, + constraint = "๋ณ€์ˆ˜๊ฐ’์€ ์œ ํ•œํ•œ ์ˆซ์ž์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + } + } + is String -> { + if (value.length > 1000) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "variables.$variableName", + value = value.length, + constraint = "๋ฌธ์ž์—ด ๋ณ€์ˆ˜๊ฐ’์€ ์ตœ๋Œ€ 1000์ž๊นŒ์ง€ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค" + ) + } + } + is Boolean -> { + // Boolean ํƒ€์ž…์€ ํ•ญ์ƒ ์œ ํšจ + } + else -> { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "variables.$variableName", + value = value::class.simpleName, + constraint = "์ง€์›๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜ ํƒ€์ž…์ž…๋‹ˆ๋‹ค. Number, String, Boolean๋งŒ ํ—ˆ์šฉ๋ฉ๋‹ˆ๋‹ค" + ) + } + } + } + + /** + * ์ˆ˜์‹ ๋ฌธ์ž์—ด์˜ ๊ธฐ๋ณธ์ ์ธ ๊ตฌ๋ฌธ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ๊ฒ€์‚ฌํ•  ์ˆ˜์‹ + * @throws ValidationException ๊ตฌ๋ฌธ ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ + */ + fun validateSyntax(formula: String) { + // ๊ด„ํ˜ธ ๊ท ํ˜• ๊ฒ€์‚ฌ + var parenCount = 0 + var braceCount = 0 + + for (char in formula) { + when (char) { + '(' -> parenCount++ + ')' -> { + parenCount-- + if (parenCount < 0) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "formula", + value = formula, + constraint = "๊ด„ํ˜ธ๊ฐ€ ๊ท ํ˜•์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + ) + } + } + '{' -> braceCount++ + '}' -> { + braceCount-- + if (braceCount < 0) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "formula", + value = formula, + constraint = "์ค‘๊ด„ํ˜ธ๊ฐ€ ๊ท ํ˜•์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + ) + } + } + } + } + + if (parenCount != 0) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "formula", + value = formula, + constraint = "๊ด„ํ˜ธ๊ฐ€ ๊ท ํ˜•์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + ) + } + + if (braceCount != 0) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "formula", + value = formula, + constraint = "์ค‘๊ด„ํ˜ธ๊ฐ€ ๊ท ํ˜•์ด ๋งž์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + ) + } + } + + /** + * ๊ณ„์‚ฐ ๋ณต์žก๋„๋ฅผ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ๋ถ„์„ํ•  ์ˆ˜์‹ + * @return ๋ณต์žก๋„ ์ ์ˆ˜ (๋†’์„์ˆ˜๋ก ๋ณต์žก) + */ + fun estimateComplexity(formula: String): Int { + var complexity = 0 + + // ์ˆ˜์‹ ๊ธธ์ด์— ๋”ฐ๋ฅธ ๊ธฐ๋ณธ ๋ณต์žก๋„ + complexity += formula.length / 10 + + // ์—ฐ์‚ฐ์ž ๊ฐœ์ˆ˜ (๊ธธ์ด์ˆœ์œผ๋กœ ์ •๋ ฌํ•˜์—ฌ ๊ฒน์น˜๋Š” ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ) + val operators = listOf("<=", ">=", "==", "!=", "&&", "||", "+", "-", "*", "/", "^", "%", "<", ">") + var formulaForCounting = formula + var totalOperatorCount = 0 + + // ๊ธด ์—ฐ์‚ฐ์ž๋ถ€ํ„ฐ ๊ฒ€์‚ฌํ•˜์—ฌ ๊ฒน์น˜๋Š” ์—ฐ์‚ฐ์ž ๋ฌธ์ œ ํ•ด๊ฒฐ + operators.forEach { op -> + val regex = Regex(Regex.escape(op)) + val matches = regex.findAll(formulaForCounting).toList() + totalOperatorCount += matches.size + + // ์ฐพ์€ ์—ฐ์‚ฐ์ž๋ฅผ ๊ณต๋ฐฑ์œผ๋กœ ์น˜ํ™˜ํ•˜์—ฌ ์ค‘๋ณต ์นด์šดํŒ… ๋ฐฉ์ง€ + formulaForCounting = formulaForCounting.replace(regex, " ".repeat(op.length)) + } + + complexity += totalOperatorCount + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฐœ์ˆ˜ + val functionPattern = Regex("[a-zA-Z]+\\(") + complexity += functionPattern.findAll(formula).count() * 2 + + // ์ค‘์ฒฉ ๊ด„ํ˜ธ ๊นŠ์ด + var maxDepth = 0 + var currentDepth = 0 + for (char in formula) { + when (char) { + '(' -> { + currentDepth++ + maxDepth = maxOf(maxDepth, currentDepth) + } + ')' -> currentDepth-- + } + } + complexity += maxDepth * 3 + + return complexity + } + + /** + * ๊ณ„์‚ฐ ๋ณต์žก๋„๊ฐ€ ํ—ˆ์šฉ ๋ฒ”์œ„ ๋‚ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ๊ฒ€์‚ฌํ•  ์ˆ˜์‹ + * @param maxComplexity ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ๋ณต์žก๋„ + * @throws ValidationException ๋ณต์žก๋„๊ฐ€ ๋„ˆ๋ฌด ๋†’์€ ๊ฒฝ์šฐ + */ + fun validateComplexity(formula: String, maxComplexity: Int = 1000) { + val complexity = estimateComplexity(formula) + + if (complexity > maxComplexity) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "formula", + value = complexity, + constraint = "์ˆ˜์‹์˜ ๋ณต์žก๋„๊ฐ€ ๋„ˆ๋ฌด ๋†’์Šต๋‹ˆ๋‹ค (์ตœ๋Œ€: $maxComplexity, ํ˜„์žฌ: $complexity)" + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/specifications/CalculationValiditySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/specifications/CalculationValiditySpec.kt new file mode 100644 index 00000000..8c768a00 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/specifications/CalculationValiditySpec.kt @@ -0,0 +1,511 @@ +package hs.kr.entrydsm.domain.calculator.specifications + +import hs.kr.entrydsm.domain.calculator.entities.CalculationSession +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * ๊ณ„์‚ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ช…์„ธ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Specification ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๊ณ„์‚ฐ ์š”์ฒญ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” + * ๋ณตํ•ฉ์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ๊ตฌ๋ฌธ ๊ฒ€์ฆ, ์˜๋ฏธ ๊ฒ€์ฆ, + * ๋ณด์•ˆ ๊ฒ€์ฆ ๋“ฑ์„ ํ†ตํ•ด ๊ณ„์‚ฐ์˜ ์‹คํ–‰ ๊ฐ€๋Šฅ์„ฑ์„ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Specification( + name = "CalculationValidity", + description = "๊ณ„์‚ฐ ์š”์ฒญ์˜ ์œ ํšจ์„ฑ๊ณผ ์‹คํ–‰ ๊ฐ€๋Šฅ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ", + domain = "calculator", + priority = hs.kr.entrydsm.global.annotation.specification.type.Priority.HIGH +) +class CalculationValiditySpec { + + companion object { + private const val MAX_EXPRESSION_LENGTH = 10000 + private const val MAX_VARIABLE_COUNT = 100 + private const val MAX_NESTING_DEPTH = 50 + private const val MAX_FUNCTION_ARGUMENTS = 20 + + // ํ—ˆ์šฉ๋œ ๋ฌธ์ž๋“ค + private val ALLOWED_CHARACTERS = setOf( + // ์ˆซ์ž์™€ ์†Œ์ˆ˜์  + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.', + // ์—ฐ์‚ฐ์ž + '+', '-', '*', '/', '%', '^', '=', '!', '<', '>', '&', '|', + // ๊ด„ํ˜ธ + '(', ')', + // ํ•จ์ˆ˜์™€ ๋ณ€์ˆ˜ + '_', + // ๊ณต๋ฐฑ๊ณผ ๊ตฌ๋ถ„์ž + ' ', '\t', ',', ';' + ) + ('a'..'z').toSet() + ('A'..'Z').toSet() + + // ํ—ˆ์šฉ๋œ ์—ฐ์‚ฐ์ž๋“ค + private val ALLOWED_OPERATORS = setOf( + "+", "-", "*", "/", "%", "^", + "==", "!=", "<", "<=", ">", ">=", + "&&", "||", "!" + ) + + // ํ—ˆ์šฉ๋œ ํ•จ์ˆ˜๋“ค + private val ALLOWED_FUNCTIONS = setOf( + "ABS", "SQRT", "ROUND", "MIN", "MAX", "SUM", "AVG", "AVERAGE", + "IF", "POW", "LOG", "LOG10", "EXP", "SIN", "COS", "TAN", + "ASIN", "ACOS", "ATAN", "ATAN2", "SINH", "COSH", "TANH", + "ASINH", "ACOSH", "ATANH", "FLOOR", "CEIL", "CEILING", + "TRUNCATE", "TRUNC", "SIGN", "RANDOM", "RAND", "RADIANS", + "DEGREES", "PI", "E", "MOD", "GCD", "LCM", "FACTORIAL", + "COMBINATION", "COMB", "PERMUTATION", "PERM" + ) + + // ๊ธˆ์ง€๋œ ํŒจํ„ด๋“ค + private val FORBIDDEN_PATTERNS = listOf( + Regex("\\beval\\b", RegexOption.IGNORE_CASE), + Regex("\\bexec\\b", RegexOption.IGNORE_CASE), + Regex("\\bsystem\\b", RegexOption.IGNORE_CASE), + Regex("\\bprocess\\b", RegexOption.IGNORE_CASE), + Regex("\\bfile\\b", RegexOption.IGNORE_CASE), + Regex("\\bimport\\b", RegexOption.IGNORE_CASE), + Regex("\\binclude\\b", RegexOption.IGNORE_CASE), + Regex("__.*__"), // Python dunder methods + Regex("\\$\\{.*\\}"), // Shell expansion + Regex("<%.*%>") // Template injection + ) + + // ์œ ํšจํ•œ ๋ณ€์ˆ˜๋ช… ํŒจํ„ด + private val VALID_VARIABLE_PATTERN = Regex("^[a-zA-Z_][a-zA-Z0-9_]*$") + + // ์œ ํšจํ•œ ์ˆซ์ž ํŒจํ„ด + private val VALID_NUMBER_PATTERN = Regex("^-?\\d+(\\.\\d+)?([eE][+-]?\\d+)?$") + + private const val SPEC_NAME = "CalculationValiditySpec" + private const val VALIDATION_RULE_COUNT = 6 + private val VALIDATION_LAYERS = listOf("syntax", "security", "complexity", "variables", "functions", "semantics") + private val RISK_FACTORS = listOf("length", "complexity", "functions", "forbidden_patterns") + } + + /** + * ๊ณ„์‚ฐ ์š”์ฒญ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ฒ€์ฆํ•  ๊ณ„์‚ฐ ์š”์ฒญ + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ (์„ ํƒ์ ) + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isSatisfiedBy(request: CalculationRequest, session: CalculationSession? = null): Boolean { + return try { + validateBasicSyntax(request.formula) && + validateSecurity(request.formula) && + validateComplexity(request.formula) && + validateVariables(request, session) && + validateFunctions(request.formula) && + validateSemantics(request.formula) + } catch (e: Exception) { + throw CalculatorException( + errorCode = ErrorCode.VALIDATION_EXCEPTION, + formula = request.formula, + message = "๊ณ„์‚ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${e.message}", + cause = e + ) + } + } + + /** + * ํ‘œํ˜„์‹์˜ ๊ธฐ๋ณธ ๊ตฌ๋ฌธ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๊ฒ€์ฆํ•  ํ‘œํ˜„์‹ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateBasicSyntax(expression: String): Boolean { + if (expression.isBlank() || expression.length > MAX_EXPRESSION_LENGTH) { + return false + } + + // ํ—ˆ์šฉ๋œ ๋ฌธ์ž๋งŒ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธ + if (!expression.all { it in ALLOWED_CHARACTERS }) { + return false + } + + // ๊ธฐ๋ณธ ๊ตฌ๋ฌธ ๊ฒ€์‚ฌ + return validateParentheses(expression) && + validateOperators(expression) && + validateNumbers(expression) + } + + /** + * ํ‘œํ˜„์‹์˜ ๋ณด์•ˆ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๊ฒ€์ฆํ•  ํ‘œํ˜„์‹ + * @return ์•ˆ์ „ํ•˜๋ฉด true + */ + fun validateSecurity(expression: String): Boolean { + // ๊ธˆ์ง€๋œ ํŒจํ„ด ๊ฒ€์‚ฌ + if (FORBIDDEN_PATTERNS.any { it.containsMatchIn(expression) }) { + return false + } + + // ์Šคํฌ๋ฆฝํŠธ ์ธ์ ์…˜ ํŒจํ„ด ๊ฒ€์‚ฌ + if (containsScriptInjection(expression)) { + return false + } + + // ๊ณผ๋„ํ•œ ์žฌ๊ท€๋‚˜ ๋ฌดํ•œ ๋ฃจํ”„ ๊ฐ€๋Šฅ์„ฑ ๊ฒ€์‚ฌ + if (containsSuspiciousRecursion(expression)) { + return false + } + + return true + } + + /** + * ํ‘œํ˜„์‹์˜ ๋ณต์žก๋„๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๊ฒ€์ฆํ•  ํ‘œํ˜„์‹ + * @return ์ ์ ˆํ•œ ๋ณต์žก๋„๋ฉด true + */ + fun validateComplexity(expression: String): Boolean { + val nestingDepth = calculateNestingDepth(expression) + if (nestingDepth > MAX_NESTING_DEPTH) { + return false + } + + val functionCount = countFunctions(expression) + if (functionCount > 50) { // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฐœ์ˆ˜ ์ œํ•œ + return false + } + + val operatorCount = countOperators(expression) + if (operatorCount > 1000) { // ์—ฐ์‚ฐ์ž ๊ฐœ์ˆ˜ ์ œํ•œ + return false + } + + return true + } + + /** + * ๋ณ€์ˆ˜ ์‚ฌ์šฉ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ณ„์‚ฐ ์š”์ฒญ + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateVariables(request: CalculationRequest, session: CalculationSession?): Boolean { + val usedVariables = extractVariables(request.formula) + + // ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ ์ œํ•œ + if (usedVariables.size > MAX_VARIABLE_COUNT) { + return false + } + + // ๋ณ€์ˆ˜๋ช… ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + if (!usedVariables.all { VALID_VARIABLE_PATTERN.matches(it) }) { + return false + } + + // ์„ธ์…˜ ์ปจํ…์ŠคํŠธ์—์„œ ๋ณ€์ˆ˜ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ + if (session != null) { + val providedVariables = request.variables + session.variables + if (!usedVariables.all { it in providedVariables }) { + return false + } + } + + return true + } + + /** + * ํ•จ์ˆ˜ ์‚ฌ์šฉ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๊ฒ€์ฆํ•  ํ‘œํ˜„์‹ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateFunctions(expression: String): Boolean { + val usedFunctions = extractFunctions(expression) + + // ํ—ˆ์šฉ๋œ ํ•จ์ˆ˜๋งŒ ์‚ฌ์šฉํ–ˆ๋Š”์ง€ ํ™•์ธ + if (!usedFunctions.all { it.uppercase() in ALLOWED_FUNCTIONS }) { + return false + } + + // ํ•จ์ˆ˜ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ ๊ฒ€์ฆ + return validateFunctionArguments(expression) + } + + /** + * ํ‘œํ˜„์‹์˜ ์˜๋ฏธ๋ก ์  ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๊ฒ€์ฆํ•  ํ‘œํ˜„์‹ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateSemantics(expression: String): Boolean { + // 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ ๊ฐ€๋Šฅ์„ฑ ๊ฒ€์‚ฌ + if (containsDivisionByZero(expression)) { + return false + } + + // ์ •์˜์—ญ ์˜ค๋ฅ˜ ๊ฐ€๋Šฅ์„ฑ ๊ฒ€์‚ฌ + if (containsDomainErrors(expression)) { + return false + } + + // ํƒ€์ž… ๋ถˆ์ผ์น˜ ๊ฐ€๋Šฅ์„ฑ ๊ฒ€์‚ฌ + if (containsTypeMismatches(expression)) { + return false + } + + return true + } + + /** + * ํ‘œํ˜„์‹์—์„œ ๋ฐœ๊ฒฌ๋œ ๋ชจ๋“  ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ๊ฒ€์ฆํ•  ๊ณ„์‚ฐ ์š”์ฒญ + * @param session ๊ณ„์‚ฐ ์„ธ์…˜ (์„ ํƒ์ ) + * @return ์˜ค๋ฅ˜ ๋ชฉ๋ก + */ + fun getValidationErrors(request: CalculationRequest, session: CalculationSession? = null): List { + val errors = mutableListOf() + + try { + if (!validateBasicSyntax(request.formula)) { + errors.add(ValidationError("SYNTAX_ERROR", "๊ธฐ๋ณธ ๊ตฌ๋ฌธ ์˜ค๋ฅ˜")) + } + + if (!validateSecurity(request.formula)) { + errors.add(ValidationError("SECURITY_ERROR", "๋ณด์•ˆ ์œ„ํ—˜ ๊ฐ์ง€")) + } + + if (!validateComplexity(request.formula)) { + errors.add(ValidationError("COMPLEXITY_ERROR", "ํ‘œํ˜„์‹์ด ๋„ˆ๋ฌด ๋ณต์žกํ•จ")) + } + + if (!validateVariables(request, session)) { + errors.add(ValidationError("VARIABLE_ERROR", "๋ณ€์ˆ˜ ์‚ฌ์šฉ ์˜ค๋ฅ˜")) + } + + if (!validateFunctions(request.formula)) { + errors.add(ValidationError("FUNCTION_ERROR", "ํ•จ์ˆ˜ ์‚ฌ์šฉ ์˜ค๋ฅ˜")) + } + + if (!validateSemantics(request.formula)) { + errors.add(ValidationError("SEMANTIC_ERROR", "์˜๋ฏธ๋ก ์  ์˜ค๋ฅ˜")) + } + } catch (e: Exception) { + errors.add(ValidationError("VALIDATION_ERROR", "๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}")) + } + + return errors + } + + /** + * ํ‘œํ˜„์‹์˜ ์œ„ํ—˜๋„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ๋ถ„์„ํ•  ํ‘œํ˜„์‹ + * @return ์œ„ํ—˜๋„ ์ ์ˆ˜ (0-100) + */ + fun calculateRiskScore(expression: String): Int { + var risk = 0 + + // ๊ธธ์ด์— ๋”ฐ๋ฅธ ์œ„ํ—˜๋„ + risk += (expression.length / 100).coerceAtMost(10) + + // ๋ณต์žก๋„์— ๋”ฐ๋ฅธ ์œ„ํ—˜๋„ + risk += (calculateNestingDepth(expression) * 2).coerceAtMost(20) + + // ํ•จ์ˆ˜ ์‚ฌ์šฉ์— ๋”ฐ๋ฅธ ์œ„ํ—˜๋„ + risk += (countFunctions(expression) * 2).coerceAtMost(20) + + // ๊ธˆ์ง€๋œ ํŒจํ„ด์— ๋”ฐ๋ฅธ ์œ„ํ—˜๋„ + val forbiddenMatches = FORBIDDEN_PATTERNS.count { it.containsMatchIn(expression) } + risk += (forbiddenMatches * 30).coerceAtMost(50) + + return risk.coerceAtMost(100) + } + + // Private helper methods + + private fun validateParentheses(expression: String): Boolean { + var depth = 0 + for (char in expression) { + when (char) { + '(' -> depth++ + ')' -> { + depth-- + if (depth < 0) return false + } + } + } + return depth == 0 + } + + private fun validateOperators(expression: String): Boolean { + if (expression.isEmpty()) return true + + // ์—ฐ์†๋œ ์—ฐ์‚ฐ์ž ๊ฒ€์‚ฌ + val hasConsecutiveOperators = Regex("[+\\-*/^%]{2,}").containsMatchIn(expression) + + // ์‹œ์ž‘ ๋ถ€๋ถ„ ์—ฐ์‚ฐ์ž ๊ฒ€์‚ฌ - ์ฒซ ๋ฌธ์ž๊ฐ€ *, /, ^, % ์ค‘ ํ•˜๋‚˜์ธ์ง€ ํ™•์ธ + val startsWithInvalidOperator = expression.first() in setOf('*', '/', '^', '%') + + // ๋ ๋ถ€๋ถ„ ์—ฐ์‚ฐ์ž ๊ฒ€์‚ฌ - ๋งˆ์ง€๋ง‰ ๋ฌธ์ž๊ฐ€ +, -, *, /, ^, % ์ค‘ ํ•˜๋‚˜์ธ์ง€ ํ™•์ธ + val endsWithOperator = expression.last() in setOf('+', '-', '*', '/', '^', '%') + + return !hasConsecutiveOperators && !startsWithInvalidOperator && !endsWithOperator + } + + private fun validateNumbers(expression: String): Boolean { + val numbers = Regex("\\b\\d+(\\.\\d+)?([eE][+-]?\\d+)?\\b").findAll(expression) + return numbers.all { + try { + it.value.toDouble() + true + } catch (e: NumberFormatException) { + false + } + } + } + + private fun calculateNestingDepth(expression: String): Int { + var depth = 0 + var maxDepth = 0 + for (char in expression) { + when (char) { + '(' -> { + depth++ + maxDepth = maxOf(maxDepth, depth) + } + ')' -> depth-- + } + } + return maxDepth + } + + private fun countFunctions(expression: String): Int { + return Regex("[a-zA-Z]\\w*\\s*\\(").findAll(expression).count() + } + + private fun countOperators(expression: String): Int { + return expression.count { it in "+-*/%^=!<>&|" } + } + + private fun extractVariables(expression: String): Set { + val variables = mutableSetOf() + val pattern = Regex("\\b[a-zA-Z_][a-zA-Z0-9_]*\\b") + + pattern.findAll(expression).forEach { match -> + val word = match.value.uppercase() + if (word !in ALLOWED_FUNCTIONS) { + variables.add(match.value) + } + } + + return variables + } + + private fun extractFunctions(expression: String): Set { + val functions = mutableSetOf() + val pattern = Regex("([a-zA-Z]\\w*)\\s*\\(") + + pattern.findAll(expression).forEach { match -> + functions.add(match.groupValues[1]) + } + + return functions + } + + private fun validateFunctionArguments(expression: String): Boolean { + val functionCalls = Regex("([a-zA-Z]\\w*)\\s*\\(([^)]*)\\)").findAll(expression) + + return functionCalls.all { match -> + val functionName = match.groupValues[1] + val argsString = match.groupValues[2].trim() + val argCount = if (argsString.isEmpty()) 0 else argsString.split(',').size + + argCount <= MAX_FUNCTION_ARGUMENTS + } + } + + private fun containsScriptInjection(expression: String): Boolean { + val injectionPatterns = listOf( + Regex(" = mapOf( + "maxExpressionLength" to MAX_EXPRESSION_LENGTH, + "maxVariableCount" to MAX_VARIABLE_COUNT, + "maxNestingDepth" to MAX_NESTING_DEPTH, + "maxFunctionArguments" to MAX_FUNCTION_ARGUMENTS, + "allowedFunctions" to ALLOWED_FUNCTIONS.size, + "allowedOperators" to ALLOWED_OPERATORS.size, + "forbiddenPatterns" to FORBIDDEN_PATTERNS.size, + "validationLayers" to VALIDATION_LAYERS + ) + + /** + * ๋ช…์„ธ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "specificationName" to SPEC_NAME, + "validationRules" to VALIDATION_RULE_COUNT, + "securityChecks" to FORBIDDEN_PATTERNS.size, + "supportedFunctions" to ALLOWED_FUNCTIONS.size, + "supportedOperators" to ALLOWED_OPERATORS.size, + "riskFactors" to RISK_FACTORS + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/CalculationRequest.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/CalculationRequest.kt new file mode 100644 index 00000000..f6294c12 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/CalculationRequest.kt @@ -0,0 +1,426 @@ +package hs.kr.entrydsm.domain.calculator.values + +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import kotlinx.serialization.SerializationException +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode + + + +/** + * ๊ณ„์‚ฐ ์š”์ฒญ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ์ˆ˜์‹ ๊ณ„์‚ฐ์— ํ•„์š”ํ•œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ํฌํ•จํ•˜๋ฉฐ, ๋ถˆ๋ณ€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * ์ˆ˜์‹ ๋ฌธ์ž์—ด๊ณผ ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ์ •๋ณด๋ฅผ ํฌํ•จํ•˜์—ฌ ๊ณ„์‚ฐ๊ธฐ๊ฐ€ ์ˆ˜ํ–‰ํ•  ์ž‘์—…์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @property formula ๊ณ„์‚ฐํ•  ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @property variables ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ๋งต + * @property options ๊ณ„์‚ฐ ์˜ต์…˜ (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +data class CalculationRequest( + val formula: String, + val variables: Map = emptyMap(), + val options: Map = emptyMap() +) { + + init { + if (formula.isBlank()) { + throw CalculatorException.emptyFormula() + } + + if (formula.length > MAX_FORMULAR_LENGTH) { + throw CalculatorException.formulaTooLong(formula, MAX_FORMULAR_LENGTH) + } + + if (variables.size > 1000) { + throw CalculatorException.tooManyVariables(variables.size, MAX_VARIABLES_SIZE) + } + } + + /** + * ์ƒˆ๋กœ์šด ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @param value ๋ณ€์ˆ˜ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด CalculationRequest + */ + fun withVariable(name: String, value: Any): CalculationRequest { + if (name.isBlank()) { + throw CalculatorException.variableNameEmpty(name) + } + return copy(variables = variables + (name to value)) + } + + /** + * ์—ฌ๋Ÿฌ ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newVariables ์ถ”๊ฐ€ํ•  ๋ณ€์ˆ˜ ๋งต + * @return ์ƒˆ๋กœ์šด CalculationRequest + */ + fun withVariables(newVariables: Map): CalculationRequest { + return copy(variables = variables + newVariables) + } + + /** + * ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ถ”๊ฐ€ํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ์˜ต์…˜ ํ‚ค + * @param value ์˜ต์…˜ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด CalculationRequest + */ + fun withOption(key: String, value: Any): CalculationRequest { + if (key.isBlank()) { + throw CalculatorException.optionKeyEmpty(key) + } + return copy(options = options + (key to value)) + } + + /** + * ์ƒˆ๋กœ์šด ์ˆ˜์‹์œผ๋กœ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newFormula ์ƒˆ๋กœ์šด ์ˆ˜์‹ + * @return ์ƒˆ๋กœ์šด CalculationRequest + */ + fun withFormula(newFormula: String): CalculationRequest { + if (newFormula.isBlank()) { + throw CalculatorException.emptyFormula() + } + return copy(formula = newFormula) + } + + /** + * ํŠน์ • ๋ณ€์ˆ˜๋ฅผ ์ œ๊ฑฐํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์ œ๊ฑฐํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ์ƒˆ๋กœ์šด CalculationRequest + */ + fun withoutVariable(name: String): CalculationRequest { + return copy(variables = variables - name) + } + + /** + * ๋ชจ๋“  ๋ณ€์ˆ˜๋ฅผ ์ œ๊ฑฐํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒˆ๋กœ์šด CalculationRequest + */ + fun withoutVariables(): CalculationRequest { + return copy(variables = emptyMap()) + } + + /** + * ํŠน์ • ์˜ต์…˜์„ ์ œ๊ฑฐํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ์ œ๊ฑฐํ•  ์˜ต์…˜ ํ‚ค + * @return ์ƒˆ๋กœ์šด CalculationRequest + */ + fun withoutOption(key: String): CalculationRequest { + return copy(options = options - key) + } + + /** + * ๋ณ€์ˆ˜๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ™•์ธํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ๋ณ€์ˆ˜๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasVariable(name: String): Boolean = name in variables + + /** + * ์˜ต์…˜์ด ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ํ™•์ธํ•  ์˜ต์…˜ ํ‚ค + * @return ์˜ต์…˜์ด ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasOption(key: String): Boolean = key in options + + /** + * ๋ณ€์ˆ˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ๋ณ€์ˆ˜ ๊ฐ’ ๋˜๋Š” null + */ + fun getVariable(name: String): Any? = variables[name] + + /** + * ์˜ต์…˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @param key ์˜ต์…˜ ํ‚ค + * @return ์˜ต์…˜ ๊ฐ’ ๋˜๋Š” null + */ + fun getOption(key: String): Any? = options[key] + + /** + * ์ˆ˜์‹์˜ ๋ณต์žก๋„๋ฅผ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณต์žก๋„ ์ ์ˆ˜ (0-100) + */ + fun estimateComplexity(): Int { + var complexity = 0 + + // ์ˆ˜์‹ ๊ธธ์ด์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += (formula.length / 10).coerceAtMost(30) + + // ์—ฐ์‚ฐ์ž ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ (๊ธด ์—ฐ์‚ฐ์ž๋ถ€ํ„ฐ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ค‘๋ณต ์นด์šดํŠธ ๋ฐฉ์ง€) + val operators = listOf("==", "!=", "<=", ">=", "&&", "||", "+", "-", "*", "/", "^", "<", ">", "!") + var remainingFormula = formula + var operatorComplexity = 0 + + operators.forEach { op -> + val occurrences = countNonOverlappingOccurrences(remainingFormula, op) + operatorComplexity += occurrences * 2 + // ์ด๋ฏธ ์นด์šดํŠธํ•œ ์—ฐ์‚ฐ์ž๋“ค์„ ๋งˆ์Šคํ‚นํ•˜์—ฌ ์ค‘๋ณต ์นด์šดํŠธ ๋ฐฉ์ง€ + remainingFormula = remainingFormula.replace(op, " ".repeat(op.length)) + } + + complexity += operatorComplexity + + // ๊ด„ํ˜ธ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += formula.count { it == '(' } * 3 + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += formula.count { it.isLetter() } * 1 + + // ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += variables.size * 2 + + return complexity.coerceAtMost(100) + } + + /** + * ๋ฌธ์ž์—ด์—์„œ ๋ถ€๋ถ„ ๋ฌธ์ž์—ด์˜ ๊ฒน์น˜์ง€ ์•Š๋Š” ๋ฐœ์ƒ ํšŸ์ˆ˜๋ฅผ ์นด์šดํŠธํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ๊ฒ€์ƒ‰ํ•  ๋ฌธ์ž์—ด + * @param substring ์ฐพ์„ ๋ถ€๋ถ„ ๋ฌธ์ž์—ด + * @return ๋ฐœ์ƒ ํšŸ์ˆ˜ + */ + private fun countNonOverlappingOccurrences(text: String, substring: String): Int { + if (substring.isEmpty()) return 0 + + var count = 0 + var startIndex = 0 + + while (true) { + val index = text.indexOf(substring, startIndex) + if (index == -1) break + + count++ + startIndex = index + substring.length // ๊ฒน์น˜์ง€ ์•Š๊ฒŒ ๋‹ค์Œ ์œ„์น˜๋กœ ์ด๋™ + } + + return count + } + + /** + * ์š”์ฒญ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValid(): Boolean { + return try { + formula.isNotBlank() && + formula.length <= 10000 && + variables.size <= 1000 && + variables.keys.all { it.isNotBlank() } + } catch (e: Exception) { + false + } + } + + /** + * ์ˆ˜์‹์— ์‚ฌ์šฉ๋  ๊ฐ€๋Šฅ์„ฑ์ด ์žˆ๋Š” ๋ณ€์ˆ˜๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + fun extractPossibleVariables(): Set { + val regex = Regex("[a-zA-Z_][a-zA-Z0-9_]*") + return regex.findAll(formula) + .map { it.value } + .filter { !ReservedKeywords.isReserved(it) } + .toSet() + } + + /** + * ๋ˆ„๋ฝ๋œ ๋ณ€์ˆ˜๋“ค์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ˆ„๋ฝ๋œ ๋ณ€์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + fun findMissingVariables(): Set { + val possibleVariables = extractPossibleVariables() + return possibleVariables - variables.keys + } + + /** + * ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜๋“ค์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + fun findUnusedVariables(): Set { + val possibleVariables = extractPossibleVariables() + return variables.keys - possibleVariables + } + + /** + * ์š”์ฒญ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "formulaLength" to formula.length, + "variableCount" to variables.size, + "optionCount" to options.size, + "estimatedComplexity" to estimateComplexity(), + "possibleVariables" to extractPossibleVariables(), + "missingVariables" to findMissingVariables(), + "unusedVariables" to findUnusedVariables(), + "isValid" to isValid() + ) + + /** + * ์š”์ฒญ์„ JSON ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * kotlinx.serialization์„ ์‚ฌ์šฉํ•˜์—ฌ ์•ˆ์ „ํ•˜๊ฒŒ ์ง๋ ฌํ™”ํ•ฉ๋‹ˆ๋‹ค. + * ํƒ€์ž… ์ •๋ณด๋ฅผ ๋ณด์กดํ•˜๋ฉด์„œ ์•ˆ์ „ํ•œ JSON ์ง๋ ฌํ™”๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @return JSON ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toJson(): String { + return try { + @Serializable + data class CalculationRequestDto( + val formula: String, + val variables: Map, + val options: Map + ) + + val dto = CalculationRequestDto( + formula = formula, + variables = variables.mapValues { (_, value) -> + when (value) { + is String -> value + is Number -> value.toString() + is Boolean -> value.toString() + else -> value.toString() + } + }, + options = options.mapValues { (_, value) -> + when (value) { + is String -> value + is Number -> value.toString() + is Boolean -> value.toString() + else -> value.toString() + } + } + ) + + Json.encodeToString(dto) + } catch (e: SerializationException) { + throw DomainException( + errorCode = ErrorCode.SERIALIZATION_FAILED, + message = "๊ณ„์‚ฐ ์š”์ฒญ JSON ์ง๋ ฌํ™” ์‹คํŒจ: ${e.message}", + cause = e, + context = mapOf( + "formula" to formula, + "variableCount" to variables.size, + "optionCount" to options.size + ) + ) + } catch (e: Exception) { + throw DomainException( + errorCode = ErrorCode.UNEXPECTED_ERROR, + message = "๊ณ„์‚ฐ ์š”์ฒญ JSON ์ง๋ ฌํ™” ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜: ${e.message}", + cause = e, + context = mapOf( + "formula" to formula, + "variableCount" to variables.size, + "optionCount" to options.size, + "exceptionType" to e.javaClass.simpleName + ) + ) + } + } + + /** + * ์š”์ฒญ์„ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + override fun toString(): String = buildString { + append("CalculationRequest(") + append("formula=\"$formula\"") + if (variables.isNotEmpty()) { + append(", variables=$variables") + } + if (options.isNotEmpty()) { + append(", options=$options") + } + append(")") + } + + companion object { + + private const val MAX_FORMULAR_LENGTH = 10000 + private const val MAX_VARIABLES_SIZE = 1000 + + /** + * ์ˆ˜์‹๋งŒ์œผ๋กœ ๊ฐ„๋‹จํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ + * @return CalculationRequest + */ + fun simple(formula: String): CalculationRequest = CalculationRequest(formula) + + /** + * ์ˆ˜์‹๊ณผ ๋ณ€์ˆ˜๋กœ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ + * @param variables ๋ณ€์ˆ˜ ๋งต + * @return CalculationRequest + */ + fun withVariables(formula: String, variables: Map): CalculationRequest = + CalculationRequest(formula, variables) + + /** + * ์ˆ˜์‹๊ณผ ๋‹จ์ผ ๋ณ€์ˆ˜๋กœ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ + * @param variableName ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @param variableValue ๋ณ€์ˆ˜ ๊ฐ’ + * @return CalculationRequest + */ + fun withVariable(formula: String, variableName: String, variableValue: Any): CalculationRequest = + CalculationRequest(formula, mapOf(variableName to variableValue)) + + /** + * ๋นˆ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (ํ…Œ์ŠคํŠธ์šฉ). + * + * @return ๋นˆ CalculationRequest + */ + fun empty(): CalculationRequest = CalculationRequest("0") + + /** + * ์—ฌ๋Ÿฌ ์š”์ฒญ์„ ๋ฐฐ์น˜๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formulas ์ˆ˜์‹ ๋ชฉ๋ก + * @return CalculationRequest ๋ชฉ๋ก + */ + fun batch(formulas: List): List = + formulas.map { CalculationRequest(it) } + + /** + * ๋ณ€์ˆ˜ ํ…œํ”Œ๋ฆฟ์„ ์‚ฌ์šฉํ•˜์—ฌ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param template ์ˆ˜์‹ ํ…œํ”Œ๋ฆฟ + * @param variables ๋ณ€์ˆ˜ ๋งต + * @return CalculationRequest + */ + fun fromTemplate(template: String, variables: Map): CalculationRequest = + CalculationRequest(template, variables) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/CalculationResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/CalculationResult.kt new file mode 100644 index 00000000..b2a8f61d --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/CalculationResult.kt @@ -0,0 +1,461 @@ +package hs.kr.entrydsm.domain.calculator.values + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import kotlinx.serialization.Serializable +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json + +/** + * ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ์ˆ˜์‹ ๊ณ„์‚ฐ์˜ ๊ฒฐ๊ณผ์™€ ํ•จ๊ป˜ ์‹คํ–‰ ํ†ต๊ณ„, ์ค‘๊ฐ„ ๊ณผ์ • ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * ๋ถˆ๋ณ€์„ฑ์„ ๋ณด์žฅํ•˜๋ฉฐ, ๊ณ„์‚ฐ ์„ฑ๊ณต ์—ฌ๋ถ€์™€ ๊ด€๋ จ๋œ ๋‹ค์–‘ํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @property result ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๊ฐ’ + * @property executionTimeMs ์‹คํ–‰ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + * @property formula ์›๋ณธ ์ˆ˜์‹ + * @property variables ์‚ฌ์šฉ๋œ ๋ณ€์ˆ˜ ๋งต + * @property steps ์‹คํ–‰ ๋‹จ๊ณ„ ๋ชฉ๋ก + * @property ast ์ƒ์„ฑ๋œ AST ๋…ธ๋“œ (์„ ํƒ์‚ฌํ•ญ) + * @property errors ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜ ๋ชฉ๋ก + * @property warnings ๊ฒฝ๊ณ  ๋ชฉ๋ก + * @property metadata ์ถ”๊ฐ€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +data class CalculationResult( + val result: Any?, + val executionTimeMs: Long, + val formula: String, + val variables: Map = emptyMap(), + val steps: List = emptyList(), + val ast: ASTNode? = null, + val errors: List = emptyList(), + val warnings: List = emptyList(), + val metadata: Map = emptyMap() +) { + + init { + if (executionTimeMs < 0) { + throw CalculatorException.executionTimeNegative(executionTimeMs) + } + + if (formula.isBlank()) { + throw CalculatorException.emptyFormula() + } + } + + /** + * ๊ณ„์‚ฐ์ด ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณตํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSuccess(): Boolean = result != null && errors.isEmpty() + + /** + * ๊ณ„์‚ฐ์ด ์‹คํŒจํ–ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹คํŒจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isFailure(): Boolean = !isSuccess() + + /** + * ๊ฒฝ๊ณ ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฒฝ๊ณ ๊ฐ€ ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasWarnings(): Boolean = warnings.isNotEmpty() + + /** + * ๊ฒฐ๊ณผ๊ฐ€ ์ˆซ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆซ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isNumericResult(): Boolean = result is Number + + /** + * ๊ฒฐ๊ณผ๊ฐ€ ๋ถˆ๋ฆฐ๊ฐ’์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ถˆ๋ฆฐ๊ฐ’์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isBooleanResult(): Boolean = result is Boolean + + /** + * ๊ฒฐ๊ณผ๊ฐ€ ๋ฌธ์ž์—ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฌธ์ž์—ด์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isStringResult(): Boolean = result is String + + /** + * ๊ฒฐ๊ณผ๋ฅผ Double๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Double ๊ฐ’ ๋˜๋Š” null + */ + fun asDouble(): Double? = when (result) { + is Double -> result + is Int -> result.toDouble() + is Float -> result.toDouble() + is Long -> result.toDouble() + else -> null + } + + /** + * ๊ฒฐ๊ณผ๋ฅผ Int๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Int ๊ฐ’ ๋˜๋Š” null + */ + fun asInt(): Int? = when (result) { + is Int -> result + is Double -> if (result.isFinite() && result == result.toInt().toDouble()) result.toInt() else null + is Float -> if (result.isFinite() && result == result.toInt().toFloat()) result.toInt() else null + is Long -> if (result in Int.MIN_VALUE..Int.MAX_VALUE) result.toInt() else null + else -> null + } + + /** + * ๊ฒฐ๊ณผ๋ฅผ Boolean์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Boolean ๊ฐ’ ๋˜๋Š” null + */ + fun asBoolean(): Boolean? = when (result) { + is Boolean -> result + is Number -> result.toDouble() != 0.0 + is String -> result.isNotEmpty() + else -> null + } + + /** + * ๊ฒฐ๊ณผ๋ฅผ String์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return String ๊ฐ’ + */ + fun asString(): String = result?.toString() ?: NULL + + /** + * ์„ฑ๋Šฅ ๋“ฑ๊ธ‰์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๋Šฅ ๋“ฑ๊ธ‰ (A, B, C, D, F) + */ + fun getPerformanceGrade(): String = when { + executionTimeMs < 1 -> "A" + executionTimeMs < 10 -> "B" + executionTimeMs < 100 -> "C" + executionTimeMs < 1000 -> "D" + else -> "F" + } + + /** + * ๋ณต์žก๋„๋ฅผ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณต์žก๋„ ์ ์ˆ˜ (0-100) + */ + fun estimateComplexity(): Int { + var complexity = 0 + + // ์ˆ˜์‹ ๊ธธ์ด์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += (formula.length / 10).coerceAtMost(20) + + // ์‹คํ–‰ ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += when { + executionTimeMs < 1 -> 0 + executionTimeMs < 10 -> 5 + executionTimeMs < 100 -> 15 + executionTimeMs < 1000 -> 30 + else -> 50 + } + + // ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += variables.size * 2 + + // ์‹คํ–‰ ๋‹จ๊ณ„์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += steps.size * 1 + + // AST ์กด์žฌ ์—ฌ๋ถ€์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + if (ast != null) complexity += 10 + + return complexity.coerceAtMost(100) + } + + /** + * ์ƒˆ๋กœ์šด ๊ฒฝ๊ณ ๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param warning ์ถ”๊ฐ€ํ•  ๊ฒฝ๊ณ  + * @return ์ƒˆ๋กœ์šด CalculationResult + */ + fun withWarning(warning: String): CalculationResult { + return copy(warnings = warnings + warning) + } + + /** + * ์ƒˆ๋กœ์šด ์˜ค๋ฅ˜๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param error ์ถ”๊ฐ€ํ•  ์˜ค๋ฅ˜ + * @return ์ƒˆ๋กœ์šด CalculationResult + */ + fun withError(error: String): CalculationResult { + return copy(errors = errors + error) + } + + /** + * ์ƒˆ๋กœ์šด ๋‹จ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param step ์ถ”๊ฐ€ํ•  ๋‹จ๊ณ„ + * @return ์ƒˆ๋กœ์šด CalculationResult + */ + fun withStep(step: String): CalculationResult { + return copy(steps = steps + step) + } + + /** + * ์ƒˆ๋กœ์šด ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ‚ค + * @param value ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด CalculationResult + */ + fun withMetadata(key: String, value: Any): CalculationResult { + return copy(metadata = metadata + (key to value)) + } + + /** + * AST๋ฅผ ์ถ”๊ฐ€ํ•œ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param astNode AST ๋…ธ๋“œ + * @return ์ƒˆ๋กœ์šด CalculationResult + */ + fun withAST(astNode: ASTNode): CalculationResult { + return copy(ast = astNode) + } + + /** + * ๊ฒฐ๊ณผ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "isSuccess" to isSuccess(), + "isFailure" to isFailure(), + "hasWarnings" to hasWarnings(), + "resultType" to (result?.javaClass?.simpleName ?: NULL), + "executionTimeMs" to executionTimeMs, + "performanceGrade" to getPerformanceGrade(), + "estimatedComplexity" to estimateComplexity(), + "formulaLength" to formula.length, + "variableCount" to variables.size, + "stepCount" to steps.size, + "errorCount" to errors.size, + "warningCount" to warnings.size, + "hasAST" to (ast != null), + "metadataCount" to metadata.size + ) + + /** + * ๊ฒฐ๊ณผ๋ฅผ ์š”์•ฝํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์š”์•ฝ ์ •๋ณด + */ + fun getSummary(): String = buildString { + append("๊ฒฐ๊ณผ: ${asString()}") + append(" (${result?.javaClass?.simpleName ?: NULL})") + append(", ์‹คํ–‰์‹œ๊ฐ„: ${executionTimeMs}ms") + append(", ์„ฑ๋Šฅ๋“ฑ๊ธ‰: ${getPerformanceGrade()}") + if (hasWarnings()) { + append(", ๊ฒฝ๊ณ : ${warnings.size}๊ฐœ") + } + if (isFailure()) { + append(", ์˜ค๋ฅ˜: ${errors.size}๊ฐœ") + } + } + + /** + * ๊ฒฐ๊ณผ๋ฅผ JSON ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * kotlinx.serialization์„ ์‚ฌ์šฉํ•˜์—ฌ ์•ˆ์ „ํ•˜๊ฒŒ ์ง๋ ฌํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return JSON ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toJson(): String { + @Serializable + data class CalculationResultJson( + val result: String, + val executionTimeMs: Long, + val formula: String, + val variables: Map, + val steps: List, + val errors: List, + val warnings: List, + val isSuccess: Boolean + ) + + val jsonData = CalculationResultJson( + result = asString(), + executionTimeMs = executionTimeMs, + formula = formula, + variables = variables.mapValues { it.value.toString() }, + steps = steps, + errors = errors, + warnings = warnings, + isSuccess = isSuccess() + ) + + return Json.encodeToString(jsonData) + } + + /** + * ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ธํ•˜๊ฒŒ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋ฌธ์ž์—ด + */ + fun toDetailString(): String = buildString { + appendLine("=== ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ===") + appendLine("์ˆ˜์‹: $formula") + appendLine("๊ฒฐ๊ณผ: ${asString()}") + appendLine("ํƒ€์ž…: ${result?.javaClass?.simpleName ?: NULL}") + appendLine("์‹คํ–‰ ์‹œ๊ฐ„: ${executionTimeMs}ms") + appendLine("์„ฑ๋Šฅ ๋“ฑ๊ธ‰: ${getPerformanceGrade()}") + appendLine("๋ณต์žก๋„: ${estimateComplexity()}") + appendLine("์„ฑ๊ณต ์—ฌ๋ถ€: ${isSuccess()}") + + if (variables.isNotEmpty()) { + appendLine("๋ณ€์ˆ˜:") + variables.forEach { (name, value) -> + appendLine(" $name = $value") + } + } + + if (steps.isNotEmpty()) { + appendLine("์‹คํ–‰ ๋‹จ๊ณ„:") + steps.forEachIndexed { index, step -> + appendLine(" ${index + 1}. $step") + } + } + + if (warnings.isNotEmpty()) { + appendLine("๊ฒฝ๊ณ :") + warnings.forEach { warning -> + appendLine(" - $warning") + } + } + + if (errors.isNotEmpty()) { + appendLine("์˜ค๋ฅ˜:") + errors.forEach { error -> + appendLine(" - $error") + } + } + + if (metadata.isNotEmpty()) { + appendLine("๋ฉ”ํƒ€๋ฐ์ดํ„ฐ:") + metadata.forEach { (key, value) -> + appendLine(" $key = $value") + } + } + } + + /** + * ๊ฒฐ๊ณผ๋ฅผ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + override fun toString(): String = getSummary() + + companion object { + + private const val NULL = "null" + private const val TEST = "test" + + /** + * ์„ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ๊ฒฐ๊ณผ๊ฐ’ + * @param executionTimeMs ์‹คํ–‰ ์‹œ๊ฐ„ + * @param formula ์›๋ณธ ์ˆ˜์‹ + * @return CalculationResult + */ + fun success(result: Any, executionTimeMs: Long, formula: String): CalculationResult = + CalculationResult(result, executionTimeMs, formula) + + /** + * ์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param error ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ + * @param executionTimeMs ์‹คํ–‰ ์‹œ๊ฐ„ + * @param formula ์›๋ณธ ์ˆ˜์‹ + * @return CalculationResult + */ + fun failure(error: String, executionTimeMs: Long, formula: String): CalculationResult = + CalculationResult(null, executionTimeMs, formula, errors = listOf(error)) + + /** + * ๊ฒฝ๊ณ ์™€ ํ•จ๊ป˜ ์„ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ๊ฒฐ๊ณผ๊ฐ’ + * @param warning ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ + * @param executionTimeMs ์‹คํ–‰ ์‹œ๊ฐ„ + * @param formula ์›๋ณธ ์ˆ˜์‹ + * @return CalculationResult + */ + fun successWithWarning(result: Any, warning: String, executionTimeMs: Long, formula: String): CalculationResult = + CalculationResult(result, executionTimeMs, formula, warnings = listOf(warning)) + + /** + * ํ…Œ์ŠคํŠธ์šฉ ๋นˆ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ…Œ์ŠคํŠธ์šฉ CalculationResult + */ + fun testResult(): CalculationResult = CalculationResult(null, 0, TEST) + + /** + * ์—ฌ๋Ÿฌ ๊ฒฐ๊ณผ๋ฅผ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param results ๋ณ‘ํ•ฉํ•  ๊ฒฐ๊ณผ๋“ค + * @return ๋ณ‘ํ•ฉ๋œ CalculationResult + */ + fun merge(results: List): CalculationResult { + if (results.isEmpty()) { + throw CalculatorException.mergeResultsEmpty() + } + + val firstResult = results.first() + val totalExecutionTime = results.sumOf { it.executionTimeMs } + val allVariables = results.flatMap { it.variables.entries }.associate { it.key to it.value } + val allSteps = results.flatMap { it.steps } + val allErrors = results.flatMap { it.errors } + val allWarnings = results.flatMap { it.warnings } + val allMetadata = results.flatMap { it.metadata.entries }.associate { it.key to it.value } + + return CalculationResult( + result = results.lastOrNull { it.isSuccess() }?.result, + executionTimeMs = totalExecutionTime, + formula = results.joinToString(" -> ") { it.formula }, + variables = allVariables, + steps = allSteps, + errors = allErrors, + warnings = allWarnings, + metadata = allMetadata + ) + } + + /** + * ๊ฒฐ๊ณผ๋ฅผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + * + * @param result1 ์ฒซ ๋ฒˆ์งธ ๊ฒฐ๊ณผ + * @param result2 ๋‘ ๋ฒˆ์งธ ๊ฒฐ๊ณผ + * @return ๋น„๊ต ๊ฒฐ๊ณผ ๋งต + */ + fun compare(result1: CalculationResult, result2: CalculationResult): Map = mapOf( + "result1" to result1.asString(), + "result2" to result2.asString(), + "resultsEqual" to (result1.result == result2.result), + "executionTimeDiff" to (result2.executionTimeMs - result1.executionTimeMs), + "performanceComparison" to "${result1.getPerformanceGrade()} vs ${result2.getPerformanceGrade()}", + "complexityDiff" to (result2.estimateComplexity() - result1.estimateComplexity()), + "bothSuccess" to (result1.isSuccess() && result2.isSuccess()), + "bothFailure" to (result1.isFailure() && result2.isFailure()) + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/CalculationStep.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/CalculationStep.kt new file mode 100644 index 00000000..f03e834f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/CalculationStep.kt @@ -0,0 +1,386 @@ +package hs.kr.entrydsm.domain.calculator.values + +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException + +/** + * ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ์˜ ๊ฐœ๋ณ„ ๋‹จ๊ณ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ๊ฐ ๊ณ„์‚ฐ ๋‹จ๊ณ„๋Š” ์‹คํ–‰ํ•  ์ˆ˜์‹๊ณผ ์„ ํƒ์ ์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  ๋ณ€์ˆ˜๋ช…์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ CalculationStep DTO๋ฅผ DDD ๊ฐ’ ๊ฐ์ฒด๋กœ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @property stepName ๋‹จ๊ณ„์˜ ์ด๋ฆ„ (์„ ํƒ์‚ฌํ•ญ) + * @property formula ํ•ด๋‹น ๋‹จ๊ณ„์—์„œ ๊ณ„์‚ฐํ•  ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @property resultVariable ์ด ๋‹จ๊ณ„์˜ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ์ €์žฅํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.21 + */ +data class CalculationStep( + val stepName: String? = null, + val formula: String, + val resultVariable: String? = null +) { + + init { + if (formula.isBlank()) { + throw CalculatorException.emptyFormula() + } + + if (formula.length > MAX_FORMULAR_LENGTH) { + throw CalculatorException.formulaTooLong(formula, MAX_FORMULAR_LENGTH) + } + + stepName?.let { name -> + if (name.isBlank()) { + throw CalculatorException.stepNameEmpty(name) + } + + if (name.length > MAX_STEP_LENGTH) { + throw CalculatorException.stepNameTooLong(name.length, MAX_STEP_LENGTH) + } + } + + resultVariable?.let { varName -> + if (varName.isBlank()) { + throw CalculatorException.resultVariableNameEmpty(varName) + } + + if (!isValidVariableName(varName)) { + throw CalculatorException.resultVariableNameInvalid(varName) + } + } + } + + /** + * ์ƒˆ๋กœ์šด ๋‹จ๊ณ„ ์ด๋ฆ„์„ ๊ฐ€์ง„ ๋‹จ๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newStepName ์ƒˆ๋กœ์šด ๋‹จ๊ณ„ ์ด๋ฆ„ + * @return ์ƒˆ๋กœ์šด CalculationStep + */ + fun withStepName(newStepName: String?): CalculationStep { + return copy(stepName = newStepName) + } + + /** + * ์ƒˆ๋กœ์šด ์ˆ˜์‹์„ ๊ฐ€์ง„ ๋‹จ๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newFormula ์ƒˆ๋กœ์šด ์ˆ˜์‹ + * @return ์ƒˆ๋กœ์šด CalculationStep + */ + fun withFormula(newFormula: String): CalculationStep { + if (newFormula.isBlank()) { + throw CalculatorException.emptyFormula() + } + return copy(formula = newFormula) + } + + /** + * ์ƒˆ๋กœ์šด ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช…์„ ๊ฐ€์ง„ ๋‹จ๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newResultVariable ์ƒˆ๋กœ์šด ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช… + * @return ์ƒˆ๋กœ์šด CalculationStep + */ + fun withResultVariable(newResultVariable: String?): CalculationStep { + newResultVariable?.let { varName -> + if (!isValidVariableName(varName)) { + throw CalculatorException.resultVariableNameInvalid(varName) + } + } + return copy(resultVariable = newResultVariable) + } + + /** + * ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ฅผ ์ œ๊ฑฐํ•œ ๋‹จ๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒˆ๋กœ์šด CalculationStep + */ + fun withoutResultVariable(): CalculationStep { + return copy(resultVariable = null) + } + + /** + * ์ด ๋‹จ๊ณ„๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ๋ณ€์ˆ˜์— ์ €์žฅํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๊ฐ€ ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasResultVariable(): Boolean = resultVariable != null + + /** + * ์ด ๋‹จ๊ณ„๊ฐ€ ์ด๋ฆ„์„ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ๊ณ„ ์ด๋ฆ„์ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasStepName(): Boolean = stepName != null + + /** + * ์ˆ˜์‹์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ณ€์ˆ˜๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ฌ์šฉ๋˜๋Š” ๋ณ€์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + fun extractVariables(): Set { + // ์ค‘๊ด„ํ˜ธ๋กœ ๋‘˜๋Ÿฌ์‹ธ์ธ ๋ณ€์ˆ˜ ํŒจํ„ด๊ณผ ์ผ๋ฐ˜ ์‹๋ณ„์ž ํŒจํ„ด + val variablePatterns = listOf( + Regex("\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}"), // {๋ณ€์ˆ˜๋ช…} + Regex("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b") // ์ผ๋ฐ˜ ์‹๋ณ„์ž + ) + + val variables = mutableSetOf() + + variablePatterns.forEach { pattern -> + pattern.findAll(formula).forEach { match -> + val variable = if (match.groups.size > 1) match.groups[1]?.value else match.value + if (variable != null && !ReservedKeywords.isReserved(variable)) { + variables.add(variable) + } + } + } + + return variables + } + + /** + * ์ˆ˜์‹์˜ ๋ณต์žก๋„๋ฅผ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณต์žก๋„ ์ ์ˆ˜ + */ + fun estimateComplexity(): Int { + var complexity = 0 + + // ์ˆ˜์‹ ๊ธธ์ด์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += (formula.length / 10).coerceAtMost(50) + + // ์—ฐ์‚ฐ์ž ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + val operators = listOf("+", "-", "*", "/", "^", "%", "==", "!=", "<", ">", "<=", ">=", "&&", "||", "!") + operators.forEach { op -> + complexity += formula.split(op).size - 1 + } + + // ๊ด„ํ˜ธ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ (์ค‘์ฒฉ ๊ณ ๋ ค) + var maxDepth = 0 + var currentDepth = 0 + for (char in formula) { + when (char) { + '(' -> { + currentDepth++ + maxDepth = maxOf(maxDepth, currentDepth) + } + ')' -> currentDepth-- + } + } + complexity += maxDepth * 3 + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + val functionPattern = Regex("[a-zA-Z]+\\(") + complexity += functionPattern.findAll(formula).count() * 5 + + // ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += extractVariables().size * 2 + + return complexity + } + + /** + * ๋‹จ๊ณ„์˜ ํ‘œ์‹œ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ๊ณ„ ์ด๋ฆ„์ด ์žˆ์œผ๋ฉด ๋‹จ๊ณ„ ์ด๋ฆ„, ์—†์œผ๋ฉด ๊ธฐ๋ณธ ์ด๋ฆ„ + */ + fun getDisplayName(): String { + return stepName ?: "๊ณ„์‚ฐ ๋‹จ๊ณ„" + } + + /** + * ๋‹จ๊ณ„์˜ ์š”์•ฝ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์š”์•ฝ ์ •๋ณด ๋ฌธ์ž์—ด + */ + fun getSummary(): String = buildString { + append(getDisplayName()) + append(": ") + append(if (formula.length > 50) "${formula.take(47)}..." else formula) + resultVariable?.let { append(" โ†’ $it") } + } + + /** + * ๋‹จ๊ณ„์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValid(): Boolean { + return try { + formula.isNotBlank() && + formula.length <= 10000 && + (stepName?.isNotBlank() != false) && + (stepName?.length ?: 0) <= 100 && + (resultVariable?.let { isValidVariableName(it) } != false) + } catch (e: Exception) { + false + } + } + + /** + * ๋ณ€์ˆ˜๋ช…์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param variableName ํ™•์ธํ•  ๋ณ€์ˆ˜๋ช… + * @return ์œ ํšจํ•œ ๋ณ€์ˆ˜๋ช…์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + private fun isValidVariableName(variableName: String): Boolean { + if (variableName.isBlank()) return false + + // ๋ณ€์ˆ˜๋ช…์€ ์•ŒํŒŒ๋ฒณ, ์ˆซ์ž, ์–ธ๋”์Šค์ฝ”์–ด๋งŒ ํ—ˆ์šฉํ•˜๊ณ  ์ˆซ์ž๋กœ ์‹œ์ž‘ํ•  ์ˆ˜ ์—†์Œ + val validPattern = Regex("^[a-zA-Z_][a-zA-Z0-9_]*$") + return variableName.matches(validPattern) && variableName.length <= 50 + } + + /** + * ๋‹จ๊ณ„์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "stepName" to (stepName ?: ""), + "formulaLength" to formula.length, + "hasResultVariable" to hasResultVariable(), + "resultVariable" to (resultVariable ?: ""), + "variableCount" to extractVariables().size, + "variables" to extractVariables(), + "complexity" to estimateComplexity(), + "isValid" to isValid() + ) + + /** + * ๋‹จ๊ณ„์˜ ๋””๋ฒ„๊ทธ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋””๋ฒ„๊ทธ ์ •๋ณด ๋ฌธ์ž์—ด + */ + fun getDebugInfo(): String = buildString { + appendLine("=== ๊ณ„์‚ฐ ๋‹จ๊ณ„ ๋””๋ฒ„๊ทธ ์ •๋ณด ===") + appendLine("๋‹จ๊ณ„ ์ด๋ฆ„: ${stepName ?: "์—†์Œ"}") + appendLine("์ˆ˜์‹: $formula") + appendLine("๊ฒฐ๊ณผ ๋ณ€์ˆ˜: ${resultVariable ?: "์—†์Œ"}") + appendLine("์ˆ˜์‹ ๊ธธ์ด: ${formula.length}") + appendLine("๋ณต์žก๋„: ${estimateComplexity()}") + appendLine("์‚ฌ์šฉ ๋ณ€์ˆ˜: ${extractVariables()}") + appendLine("์œ ํšจ์„ฑ: ${isValid()}") + } + + /** + * ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + override fun toString(): String = buildString { + append("CalculationStep(") + stepName?.let { append("stepName=\"$it\", ") } + append("formula=\"$formula\"") + resultVariable?.let { append(", resultVariable=\"$it\"") } + append(")") + } + + companion object { + /** + * ์ˆ˜์‹๋งŒ์œผ๋กœ ๊ฐ„๋‹จํ•œ ๋‹จ๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ + * @return CalculationStep + */ + fun simple(formula: String): CalculationStep { + return CalculationStep(formula = formula) + } + + /** + * ์ˆ˜์‹๊ณผ ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋กœ ๋‹จ๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ + * @param resultVariable ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช… + * @return CalculationStep + */ + fun withResult(formula: String, resultVariable: String): CalculationStep { + return CalculationStep(formula = formula, resultVariable = resultVariable) + } + + /** + * ์ด๋ฆ„๊ณผ ์ˆ˜์‹์œผ๋กœ ๋‹จ๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param stepName ๋‹จ๊ณ„ ์ด๋ฆ„ + * @param formula ์ˆ˜์‹ + * @return CalculationStep + */ + fun named(stepName: String, formula: String): CalculationStep { + return CalculationStep(stepName = stepName, formula = formula) + } + + /** + * ์™„์ „ํ•œ ์ •๋ณด๋กœ ๋‹จ๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param stepName ๋‹จ๊ณ„ ์ด๋ฆ„ + * @param formula ์ˆ˜์‹ + * @param resultVariable ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช… + * @return CalculationStep + */ + fun complete(stepName: String, formula: String, resultVariable: String): CalculationStep { + return CalculationStep(stepName = stepName, formula = formula, resultVariable = resultVariable) + } + + /** + * ๋‹จ๊ณ„ ๋ชฉ๋ก์„ ์ผ๊ด„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formulas ์ˆ˜์‹ ๋ชฉ๋ก + * @param namePrefix ๋‹จ๊ณ„ ์ด๋ฆ„ ์ ‘๋‘์‚ฌ + * @param resultPrefix ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช… ์ ‘๋‘์‚ฌ + * @return CalculationStep ๋ชฉ๋ก + */ + fun batch( + formulas: List, + namePrefix: String = "๋‹จ๊ณ„", + resultPrefix: String? = null + ): List { + return formulas.mapIndexed { index, formula -> + val stepName = "$namePrefix ${index + 1}" + val resultVariable = resultPrefix?.let { "${it}_${index + 1}" } + CalculationStep(stepName = stepName, formula = formula, resultVariable = resultVariable) + } + } + + /** + * ๋นŒ๋” ํŒจํ„ด์œผ๋กœ ๋‹จ๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return CalculationStepBuilder + */ + fun builder(): CalculationStepBuilder { + return CalculationStepBuilder() + } + + private const val MAX_FORMULAR_LENGTH = 10000 + private const val MAX_STEP_LENGTH = 100 + } + + /** + * ๊ณ„์‚ฐ ๋‹จ๊ณ„๋ฅผ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๋นŒ๋” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + class CalculationStepBuilder { + private var stepName: String? = null + private var formula: String = "" + private var resultVariable: String? = null + + fun stepName(name: String): CalculationStepBuilder { + this.stepName = name + return this + } + + fun formula(formula: String): CalculationStepBuilder { + this.formula = formula + return this + } + + fun resultVariable(variable: String): CalculationStepBuilder { + this.resultVariable = variable + return this + } + + fun build(): CalculationStep { + return CalculationStep(stepName, formula, resultVariable) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/MultiStepCalculationRequest.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/MultiStepCalculationRequest.kt new file mode 100644 index 00000000..49cbe6ee --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/MultiStepCalculationRequest.kt @@ -0,0 +1,519 @@ +package hs.kr.entrydsm.domain.calculator.values + +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * ๋‹ค๋‹จ๊ณ„ ์ˆ˜์‹ ๊ณ„์‚ฐ ์š”์ฒญ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ์—ฌ๋Ÿฌ ๊ฐœ์˜ ๊ณ„์‚ฐ ๋‹จ๊ณ„๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ์‹คํ–‰ํ•˜์—ฌ, ์ด์ „ ๋‹จ๊ณ„์˜ ๊ฒฐ๊ณผ๋ฅผ + * ๋‹ค์Œ ๋‹จ๊ณ„์—์„œ ๋ณ€์ˆ˜๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ๋ณตํ•ฉ ๊ณ„์‚ฐ ์š”์ฒญ์„ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ MultiStepCalculationRequest DTO๋ฅผ DDD ๊ฐ’ ๊ฐ์ฒด๋กœ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @property variables ๋ชจ๋“  ๋‹จ๊ณ„์—์„œ ๊ณตํ†ต์œผ๋กœ ์‚ฌ์šฉ๋  ์ดˆ๊ธฐ ๋ณ€์ˆ˜ ๋งต + * @property steps ์ˆ˜ํ–‰ํ•  ๊ณ„์‚ฐ ๋‹จ๊ณ„ ๋ชฉ๋ก + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.21 + */ +data class MultiStepCalculationRequest( + val variables: Map = emptyMap(), + val steps: List = emptyList() +) { + + init { + if (steps.isEmpty()) { + throw CalculatorException.stepsEmpty() + } + + if (steps.size > MAX_STEP_SIZE) { + throw CalculatorException.stepsTooMany(steps.size, MAX_STEP_SIZE) + } + + if (variables.size > MAX_VARIABLES_SIZE) { + throw CalculatorException.variablesTooMany(variables.size, MAX_VARIABLES_SIZE) + } + + // ๋‹จ๊ณ„๋ณ„ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + steps.forEachIndexed { index, step -> + if (step.formula.isBlank()) { + throw CalculatorException.stepFormulaEmpty(index + 1) + } + } + } + + /** + * ์ƒˆ๋กœ์šด ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @param value ๋ณ€์ˆ˜ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด MultiStepCalculationRequest + */ + fun withVariable(name: String, value: Any?): MultiStepCalculationRequest { + if (name.isBlank()) { + throw CalculatorException.variableNameEmpty(name) + } + return copy(variables = variables + (name to value)) + } + + /** + * ์—ฌ๋Ÿฌ ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newVariables ์ถ”๊ฐ€ํ•  ๋ณ€์ˆ˜ ๋งต + * @return ์ƒˆ๋กœ์šด MultiStepCalculationRequest + */ + fun withVariables(newVariables: Map): MultiStepCalculationRequest { + return copy(variables = variables + newVariables) + } + + /** + * ์ƒˆ๋กœ์šด ๋‹จ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param step ์ถ”๊ฐ€ํ•  ๊ณ„์‚ฐ ๋‹จ๊ณ„ + * @return ์ƒˆ๋กœ์šด MultiStepCalculationRequest + */ + fun withStep(step: CalculationStep): MultiStepCalculationRequest { + return copy(steps = steps + step) + } + + /** + * ์—ฌ๋Ÿฌ ๋‹จ๊ณ„๋ฅผ ์ถ”๊ฐ€ํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newSteps ์ถ”๊ฐ€ํ•  ๊ณ„์‚ฐ ๋‹จ๊ณ„ ๋ชฉ๋ก + * @return ์ƒˆ๋กœ์šด MultiStepCalculationRequest + */ + fun withSteps(newSteps: List): MultiStepCalculationRequest { + return copy(steps = steps + newSteps) + } + + /** + * ํŠน์ • ์œ„์น˜์— ๋‹จ๊ณ„๋ฅผ ์‚ฝ์ž…ํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ์‚ฝ์ž…ํ•  ์œ„์น˜ + * @param step ์‚ฝ์ž…ํ•  ๊ณ„์‚ฐ ๋‹จ๊ณ„ + * @return ์ƒˆ๋กœ์šด MultiStepCalculationRequest + */ + fun insertStep(index: Int, step: CalculationStep): MultiStepCalculationRequest { + if (index < 0 || index > steps.size) { + throw CalculatorException.indexOutOfRangeInclusive(index, steps.size) + } + val newSteps = steps.toMutableList() + newSteps.add(index, step) + return copy(steps = newSteps) + } + + /** + * ํŠน์ • ์œ„์น˜์˜ ๋‹จ๊ณ„๋ฅผ ์ œ๊ฑฐํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ์ œ๊ฑฐํ•  ๋‹จ๊ณ„์˜ ์œ„์น˜ + * @return ์ƒˆ๋กœ์šด MultiStepCalculationRequest + */ + fun removeStep(index: Int): MultiStepCalculationRequest { + if (index !in steps.indices) { + throw CalculatorException.indexOutOfRangeExclusive(index, steps.size) + } + + if (steps.size <= 1) { + throw CalculatorException.minStepsRequired(steps.size, 2) + } + + return copy(steps = steps.filterIndexed { i, _ -> i != index }) + } + + /** + * ํŠน์ • ๋ณ€์ˆ˜๋ฅผ ์ œ๊ฑฐํ•œ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์ œ๊ฑฐํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ์ƒˆ๋กœ์šด MultiStepCalculationRequest + */ + fun withoutVariable(name: String): MultiStepCalculationRequest { + return copy(variables = variables - name) + } + + /** + * ๋ณ€์ˆ˜๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ™•์ธํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ๋ณ€์ˆ˜๊ฐ€ ์ •์˜๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasVariable(name: String): Boolean = name in variables + + /** + * ๋ณ€์ˆ˜ ๊ฐ’์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ๋ณ€์ˆ˜ ๊ฐ’ ๋˜๋Š” null + */ + fun getVariable(name: String): Any? = variables[name] + + /** + * ํŠน์ • ์œ„์น˜์˜ ๋‹จ๊ณ„๋ฅผ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @param index ๋‹จ๊ณ„ ์œ„์น˜ + * @return ๊ณ„์‚ฐ ๋‹จ๊ณ„ + */ + fun getStep(index: Int): CalculationStep { + if (index !in steps.indices) { + throw CalculatorException.indexOutOfRangeExclusive(index, steps.size) + } + + return steps[index] + } + + /** + * ์ „์ฒด ๊ณ„์‚ฐ์˜ ๋ณต์žก๋„๋ฅผ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณต์žก๋„ ์ ์ˆ˜ + */ + fun estimateComplexity(): Int { + var totalComplexity = 0 + + // ๋‹จ๊ณ„๋ณ„ ๋ณต์žก๋„ ํ•ฉ์‚ฐ + steps.forEach { step -> + totalComplexity += estimateStepComplexity(step) + } + + // ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + totalComplexity += variables.size * 2 + + // ๋‹จ๊ณ„ ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ ๋ณด์ • + totalComplexity += steps.size * 5 + + return totalComplexity + } + + /** + * ๋‹จ์ผ ๋‹จ๊ณ„์˜ ๋ณต์žก๋„๋ฅผ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param step ๊ณ„์‚ฐ ๋‹จ๊ณ„ + * @return ๋‹จ๊ณ„ ๋ณต์žก๋„ ์ ์ˆ˜ + */ + private fun estimateStepComplexity(step: CalculationStep): Int { + var complexity = 0 + + // ์ˆ˜์‹ ๊ธธ์ด์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += (step.formula.length / 10).coerceAtMost(30) + + // ์—ฐ์‚ฐ์ž ๊ฐœ์ˆ˜ + val operators = listOf("+", "-", "*", "/", "^", "==", "!=", "<", ">", "<=", ">=", "&&", "||", "!") + complexity += operators.sumOf { op -> + step.formula.split(op).size - 1 + } * 2 + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฐœ์ˆ˜ + val functionPattern = Regex("[a-zA-Z]+\\(") + complexity += functionPattern.findAll(step.formula).count() * 3 + + return complexity + } + + /** + * ๋ชจ๋“  ๋‹จ๊ณ„์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ณ€์ˆ˜๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ฌ์šฉ๋˜๋Š” ๋ณ€์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + fun extractAllVariables(): Set { + val allVariables = mutableSetOf() + + steps.forEach { step -> + allVariables.addAll(extractVariablesFromFormula(step.formula)) + } + + return allVariables + } + + /** + * ์ˆ˜์‹์—์„œ ๋ณ€์ˆ˜๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @return ๋ณ€์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + private fun extractVariablesFromFormula(formula: String): Set { + // ์ค‘๊ด„ํ˜ธ๋กœ ๋‘˜๋Ÿฌ์‹ธ์ธ ๋ณ€์ˆ˜ ํŒจํ„ด๊ณผ ์ผ๋ฐ˜ ์‹๋ณ„์ž ํŒจํ„ด + val variablePatterns = listOf( + Regex("\\{([a-zA-Z_][a-zA-Z0-9_]*)\\}"), // {๋ณ€์ˆ˜๋ช…} + Regex("\\b([a-zA-Z_][a-zA-Z0-9_]*)\\b") // ์ผ๋ฐ˜ ์‹๋ณ„์ž + ) + + val variables = mutableSetOf() + val reservedWords = setOf( + "sin", "cos", "tan", "sqrt", "log", "exp", "abs", "floor", "ceil", "round", + "min", "max", "pow", "if", "true", "false", "and", "or", "not" + ) + + variablePatterns.forEach { pattern -> + pattern.findAll(formula).forEach { match -> + val variable = if (match.groups.size > 1) match.groups[1]?.value else match.value + if (variable != null && variable !in reservedWords) { + variables.add(variable) + } + } + } + + return variables + } + + /** + * ๋ˆ„๋ฝ๋œ ๋ณ€์ˆ˜๋“ค์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ˆ„๋ฝ๋œ ๋ณ€์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + fun findMissingVariables(): Set { + val requiredVariables = extractAllVariables() + val providedVariables = variables.keys + + // ๋‹จ๊ณ„๋ณ„ ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋“ค๋„ ๊ณ ๋ ค + val resultVariables = steps.mapNotNull { it.resultVariable }.toSet() + val availableVariables = providedVariables + resultVariables + + return requiredVariables - availableVariables + } + + /** + * ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜๋“ค์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + fun findUnusedVariables(): Set { + val requiredVariables = extractAllVariables() + return variables.keys - requiredVariables + } + + /** + * ๋‹จ๊ณ„๋ณ„ ์˜์กด์„ฑ์„ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ๊ณ„๋ณ„ ์˜์กด ๋ณ€์ˆ˜ ๋งต + */ + fun analyzeDependencies(): Map> { + val dependencies = mutableMapOf>() + + steps.forEachIndexed { index, step -> + dependencies[index] = extractVariablesFromFormula(step.formula) + } + + return dependencies + } + + /** + * ์ˆœํ™˜ ์˜์กด์„ฑ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * DFS ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜์—ฌ ๋ชจ๋“  ์ง๊ฐ„์ ‘ ์ˆœํ™˜ ์˜์กด์„ฑ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆœํ™˜ ์˜์กด์„ฑ์ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasCircularDependency(): Boolean { + // ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„ ๊ตฌ์ถ• + val dependencyGraph = buildDependencyGraph() + val visited = mutableSetOf() + val recursionStack = mutableSetOf() + + // ๋ชจ๋“  ๋…ธ๋“œ์—์„œ DFS ์ˆ˜ํ–‰ + for (node in dependencyGraph.keys) { + if (node !in visited) { + if (hasCycleDFS(node, dependencyGraph, visited, recursionStack)) { + return true + } + } + } + + return false + } + + /** + * ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ ๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ฅผ ํ‚ค๋กœ ํ•˜๊ณ , ํ•ด๋‹น ๋ณ€์ˆ˜๊ฐ€ ์˜์กดํ•˜๋Š” ๋ณ€์ˆ˜๋“ค์„ ๊ฐ’์œผ๋กœ ํ•˜๋Š” ๋งต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„ ๋งต + */ + private fun buildDependencyGraph(): Map> { + val graph = mutableMapOf>() + val dependencies = analyzeDependencies() + + steps.forEachIndexed { index, step -> + step.resultVariable?.let { resultVar -> + val stepDependencies = dependencies[index] ?: emptySet() + graph[resultVar] = stepDependencies + + // ์˜์กดํ•˜๋Š” ๋ณ€์ˆ˜๋“ค๋„ ๊ทธ๋ž˜ํ”„์— ์ถ”๊ฐ€ (๋นˆ ์˜์กด์„ฑ์œผ๋กœ) + stepDependencies.forEach { dep -> + if (dep !in graph) { + graph[dep] = emptySet() + } + } + } + } + + return graph + } + + /** + * DFS๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ˆœํ™˜ ์˜์กด์„ฑ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * ์žฌ๊ท€ ์Šคํƒ์„ ์‚ฌ์šฉํ•˜์—ฌ ํ˜„์žฌ ๊ฒฝ๋กœ์—์„œ ์ด๋ฏธ ๋ฐฉ๋ฌธํ•œ ๋…ธ๋“œ๋ฅผ ๋‹ค์‹œ ๋งŒ๋‚˜๋ฉด ์ˆœํ™˜์œผ๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ํ˜„์žฌ ๋…ธ๋“œ + * @param graph ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„ + * @param visited ๋ฐฉ๋ฌธํ•œ ๋…ธ๋“œ ์ง‘ํ•ฉ + * @param recursionStack ํ˜„์žฌ ์žฌ๊ท€ ๊ฒฝ๋กœ์˜ ๋…ธ๋“œ ์ง‘ํ•ฉ + * @return ์ˆœํ™˜์ด ๊ฐ์ง€๋˜๋ฉด true + */ + private fun hasCycleDFS( + node: String, + graph: Map>, + visited: MutableSet, + recursionStack: MutableSet + ): Boolean { + visited.add(node) + recursionStack.add(node) + + // ํ˜„์žฌ ๋…ธ๋“œ์˜ ๋ชจ๋“  ์˜์กด์„ฑ์„ ๊ฒ€์‚ฌ + val dependencies = graph[node] ?: emptySet() + for (dependency in dependencies) { + // ์˜์กด์„ฑ์ด ํ˜„์žฌ ์žฌ๊ท€ ์Šคํƒ์— ์žˆ์œผ๋ฉด ์ˆœํ™˜ ๊ฐ์ง€ + if (dependency in recursionStack) { + return true + } + + // ์•„์ง ๋ฐฉ๋ฌธํ•˜์ง€ ์•Š์€ ์˜์กด์„ฑ์— ๋Œ€ํ•ด ์žฌ๊ท€ DFS ์ˆ˜ํ–‰ + if (dependency !in visited) { + if (hasCycleDFS(dependency, graph, visited, recursionStack)) { + return true + } + } + } + + // ํ˜„์žฌ ๋…ธ๋“œ ์ฒ˜๋ฆฌ ์™„๋ฃŒ, ์žฌ๊ท€ ์Šคํƒ์—์„œ ์ œ๊ฑฐ + recursionStack.remove(node) + return false + } + + /** + * ์š”์ฒญ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + * @throws CalculatorException ๊ฒ€์ฆ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ + */ + fun isValid(): Boolean { + return try { + steps.isNotEmpty() && + steps.size <= 100 && + variables.size <= 1000 && + steps.all { it.formula.isNotBlank() && it.formula.length <= 10000 } && + !hasCircularDependency() + } catch (e: Exception) { + throw CalculatorException( + errorCode = ErrorCode.VALIDATION_EXCEPTION, + message = "๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: ${e.message}", + cause = e + ) + } + } + + /** + * ์š”์ฒญ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "stepCount" to steps.size, + "variableCount" to variables.size, + "totalComplexity" to estimateComplexity(), + "requiredVariables" to extractAllVariables(), + "missingVariables" to findMissingVariables(), + "unusedVariables" to findUnusedVariables(), + "hasCircularDependency" to hasCircularDependency(), + "dependencies" to analyzeDependencies(), + "isValid" to isValid() + ) + + companion object { + /** + * ๋‹จ์ผ ๋‹จ๊ณ„๋กœ ๊ฐ„๋‹จํ•œ ๋‹ค๋‹จ๊ณ„ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ + * @return MultiStepCalculationRequest + */ + fun singleStep(formula: String): MultiStepCalculationRequest { + return MultiStepCalculationRequest( + steps = listOf(CalculationStep.simple(formula)) + ) + } + + /** + * ์—ฌ๋Ÿฌ ์ˆ˜์‹์œผ๋กœ ๋‹ค๋‹จ๊ณ„ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formulas ์ˆ˜์‹ ๋ชฉ๋ก + * @return MultiStepCalculationRequest + */ + fun fromFormulas(formulas: List): MultiStepCalculationRequest { + val steps = formulas.mapIndexed { index, formula -> + CalculationStep( + stepName = "๋‹จ๊ณ„ ${index + 1}", + formula = formula, + resultVariable = if (index < formulas.size - 1) "step${index + 1}_result" else null + ) + } + return MultiStepCalculationRequest(steps = steps) + } + + /** + * ๋ณ€์ˆ˜์™€ ํ•จ๊ป˜ ๋‹ค๋‹จ๊ณ„ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param variables ์ดˆ๊ธฐ ๋ณ€์ˆ˜ ๋งต + * @param steps ๊ณ„์‚ฐ ๋‹จ๊ณ„ ๋ชฉ๋ก + * @return MultiStepCalculationRequest + */ + fun create(variables: Map, steps: List): MultiStepCalculationRequest { + return MultiStepCalculationRequest(variables, steps) + } + + /** + * ๋นŒ๋” ํŒจํ„ด์œผ๋กœ ๋‹ค๋‹จ๊ณ„ ์š”์ฒญ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return MultiStepCalculationRequestBuilder + */ + fun builder(): MultiStepCalculationRequestBuilder { + return MultiStepCalculationRequestBuilder() + } + + private const val MAX_STEP_SIZE = 100 + private const val MAX_VARIABLES_SIZE = 1000 + } + + /** + * ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ์š”์ฒญ์„ ๊ตฌ์„ฑํ•˜๊ธฐ ์œ„ํ•œ ๋นŒ๋” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + class MultiStepCalculationRequestBuilder { + private val variables = mutableMapOf() + private val steps = mutableListOf() + + fun variable(name: String, value: Any?): MultiStepCalculationRequestBuilder { + variables[name] = value + return this + } + + fun variables(vars: Map): MultiStepCalculationRequestBuilder { + variables.putAll(vars) + return this + } + + fun step(step: CalculationStep): MultiStepCalculationRequestBuilder { + steps.add(step) + return this + } + + fun step(formula: String): MultiStepCalculationRequestBuilder { + steps.add(CalculationStep.simple(formula)) + return this + } + + fun step(stepName: String, formula: String, resultVariable: String? = null): MultiStepCalculationRequestBuilder { + steps.add(CalculationStep(stepName, formula, resultVariable)) + return this + } + + fun build(): MultiStepCalculationRequest { + return MultiStepCalculationRequest(variables.toMap(), steps.toList()) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/PerformanceRecommendation.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/PerformanceRecommendation.kt new file mode 100644 index 00000000..62f5322e --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/PerformanceRecommendation.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.domain.calculator.values + +/** + * ์„ฑ๋Šฅ ์ตœ์ ํ™” ๊ถŒ์žฅ์‚ฌํ•ญ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * @property type ๊ถŒ์žฅ์‚ฌํ•ญ ํƒ€์ž… + * @property message ๊ถŒ์žฅ์‚ฌํ•ญ ๋ฉ”์‹œ์ง€ + * @property priority ๊ถŒ์žฅ์‚ฌํ•ญ ์šฐ์„ ์ˆœ์œ„ + * + * @author kangeunchan + * @since 2025.07.28 + */ +data class PerformanceRecommendation( + val type: RecommendationType, + val message: String, + val priority: RecommendationPriority +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/RecommendationPriority.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/RecommendationPriority.kt new file mode 100644 index 00000000..66ca7a9d --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/RecommendationPriority.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.domain.calculator.values + +/** + * ์„ฑ๋Šฅ ๊ถŒ์žฅ์‚ฌํ•ญ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.28 + */ +enum class RecommendationPriority { + LOW, MEDIUM, HIGH, CRITICAL +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/RecommendationType.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/RecommendationType.kt new file mode 100644 index 00000000..1399b9e8 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/RecommendationType.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.domain.calculator.values + +/** + * ์„ฑ๋Šฅ ๊ถŒ์žฅ์‚ฌํ•ญ์˜ ํƒ€์ž…์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.28 + */ +enum class RecommendationType { + EXECUTION_TIME, MEMORY_USAGE, CACHE_HIT_RATE, CONCURRENCY +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/ReservedKeywords.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/ReservedKeywords.kt new file mode 100644 index 00000000..d8e7731f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/values/ReservedKeywords.kt @@ -0,0 +1,213 @@ +package hs.kr.entrydsm.domain.calculator.values + +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode + +/** + * ๊ณ„์‚ฐ๊ธฐ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์˜ˆ์•ฝ์–ด๋“ค์„ ์ค‘์•™์—์„œ ๊ด€๋ฆฌํ•˜๋Š” ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ๋ชจ๋“  ์ˆ˜ํ•™ ํ•จ์ˆ˜, ์ง‘๊ณ„ ํ•จ์ˆ˜, ์˜ˆ์•ฝ์–ด๋“ค์„ ํ•˜๋‚˜์˜ ์žฅ์†Œ์—์„œ ๊ด€๋ฆฌํ•˜์—ฌ + * ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜๊ณ  ์ค‘๋ณต์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. FunctionCallNode์—์„œ ์ง€์›ํ•˜๋Š” + * ํ•จ์ˆ˜๋“ค๊ณผ ํ†ตํ•ฉํ•˜์—ฌ ์ผ๊ด€๋œ ์˜ˆ์•ฝ์–ด ๊ด€๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.03 + */ +object ReservedKeywords { + + /** + * ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค (FunctionCallNode์™€ ํ†ตํ•ฉ) + */ + val MATH_FUNCTIONS = setOf( + // ์‚ผ๊ฐํ•จ์ˆ˜ + "sin", "cos", "tan", "asin", "acos", "atan", "atan2", + // ์Œ๊ณกํ•จ์ˆ˜ + "sinh", "cosh", "tanh", "asinh", "acosh", "atanh", + // ์ง€์ˆ˜/๋กœ๊ทธ ํ•จ์ˆ˜ + "exp", "log", "log10", "log2", "ln", + // ๊ธฐ๋ณธ ์ˆ˜ํ•™ ํ•จ์ˆ˜ + "sqrt", "cbrt", "pow", "abs", "sign", + // ๋ฐ˜์˜ฌ๋ฆผ ํ•จ์ˆ˜ + "floor", "ceil", "round", "trunc", "truncate", + // ๋น„๊ต ํ•จ์ˆ˜ + "min", "max", "clamp", + // ๊ฐ๋„ ๋ณ€ํ™˜ + "radians", "degrees", + // ๊ธฐํƒ€ ์ˆ˜ํ•™ ํ•จ์ˆ˜ + "mod", "random", "rand" + ) + + /** + * ์ง‘๊ณ„ ํ•จ์ˆ˜๋“ค (FunctionCallNode์™€ ํ†ตํ•ฉ + ์ถ”๊ฐ€ ๋„๋ฉ”์ธ ํ•จ์ˆ˜) + */ + val AGGREGATE_FUNCTIONS = setOf( + // FunctionCallNode์—์„œ ์ง€์›ํ•˜๋Š” ์ง‘๊ณ„ ํ•จ์ˆ˜ + "sum", "avg", "mean", "median", "mode", + "count", "distinct", "variance", "stddev", + // ์ถ”๊ฐ€ ๋„๋ฉ”์ธ ํŠนํ™” ์ง‘๊ณ„ ํ•จ์ˆ˜ + "average", "gcd", "lcm", "factorial", + "combination", "comb", "permutation", "perm" + ) + + /** + * ๋ฌธ์ž์—ด ํ•จ์ˆ˜๋“ค (FunctionCallNode์—์„œ ์ง€์›) + */ + val STRING_FUNCTIONS = setOf( + "length", "upper", "lower", "trim", "substring", + "replace", "contains", "startswith", "endswith" + ) + + /** + * ๋…ผ๋ฆฌ ๋ฐ ์กฐ๊ฑด๋ถ€ ์˜ˆ์•ฝ์–ด๋“ค + */ + val LOGICAL_KEYWORDS = setOf( + "if", "true", "false", "and", "or", "not" + ) + + /** + * ์ƒ์ˆ˜๋“ค + */ + val CONSTANTS = setOf( + "pi", "e" + ) + + /** + * FunctionCallNode์—์„œ ์ง€์›ํ•˜๋Š” ๋ชจ๋“  ํ•จ์ˆ˜๋“ค + */ + val FUNCTION_CALL_SUPPORTED: Set = try { + FunctionCallNode.getSupportedFunctions() + } catch (e: Exception) { + // FunctionCallNode ํด๋ž˜์Šค๋ฅผ ๋กœ๋“œํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ fallback + MATH_FUNCTIONS + AGGREGATE_FUNCTIONS + STRING_FUNCTIONS + } + + /** + * ๋ชจ๋“  ์˜ˆ์•ฝ์–ด๋“ค์˜ ํ•ฉ์ง‘ํ•ฉ (FunctionCallNode ์ง€์› ํ•จ์ˆ˜ ํฌํ•จ) + */ + val ALL_RESERVED: Set = FUNCTION_CALL_SUPPORTED + LOGICAL_KEYWORDS + CONSTANTS + + /** + * ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด์ด ์˜ˆ์•ฝ์–ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param word ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ์˜ˆ์•ฝ์–ด์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isReserved(word: String): Boolean { + return word.lowercase() in ALL_RESERVED + } + + /** + * ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด์ด ์ˆ˜ํ•™ ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param word ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ์ˆ˜ํ•™ ํ•จ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isMathFunction(word: String): Boolean { + return word.lowercase() in MATH_FUNCTIONS + } + + /** + * ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด์ด ์ง‘๊ณ„ ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param word ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ์ง‘๊ณ„ ํ•จ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAggregateFunction(word: String): Boolean { + return word.lowercase() in AGGREGATE_FUNCTIONS + } + + /** + * ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด์ด ๋ฌธ์ž์—ด ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param word ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ๋ฌธ์ž์—ด ํ•จ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isStringFunction(word: String): Boolean { + return word.lowercase() in STRING_FUNCTIONS + } + + /** + * ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด์ด FunctionCallNode์—์„œ ์ง€์›ํ•˜๋Š” ํ•จ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param word ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ์ง€์›๋˜๋Š” ํ•จ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSupportedFunction(word: String): Boolean { + return word.lowercase() in FUNCTION_CALL_SUPPORTED + } + + /** + * ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด์ด ๋…ผ๋ฆฌ ํ‚ค์›Œ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param word ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ๋…ผ๋ฆฌ ํ‚ค์›Œ๋“œ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isLogicalKeyword(word: String): Boolean { + return word.lowercase() in LOGICAL_KEYWORDS + } + + /** + * ์ฃผ์–ด์ง„ ๋ฌธ์ž์—ด์ด ์ƒ์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param word ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ์ƒ์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isConstant(word: String): Boolean { + return word.lowercase() in CONSTANTS + } + + /** + * ๋ฌธ์ž์—ด ๋ชฉ๋ก์—์„œ ์˜ˆ์•ฝ์–ด๊ฐ€ ์•„๋‹Œ ๊ฒƒ๋“ค๋งŒ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. + * + * @param words ํ•„ํ„ฐ๋งํ•  ๋ฌธ์ž์—ด ์ง‘ํ•ฉ + * @return ์˜ˆ์•ฝ์–ด๊ฐ€ ์•„๋‹Œ ๋ฌธ์ž์—ด๋“ค์˜ ์ง‘ํ•ฉ + */ + fun filterNonReserved(words: Set): Set { + return words.filter { !isReserved(it) }.toSet() + } + + /** + * ์˜ˆ์•ฝ์–ด ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์˜ˆ์•ฝ์–ด ๊ฐœ์ˆ˜๋ฅผ ๋‹ด์€ ๋งต + */ + fun getStatistics(): Map = mapOf( + "mathFunctions" to MATH_FUNCTIONS.size, + "aggregateFunctions" to AGGREGATE_FUNCTIONS.size, + "stringFunctions" to STRING_FUNCTIONS.size, + "logicalKeywords" to LOGICAL_KEYWORDS.size, + "constants" to CONSTANTS.size, + "functionCallSupported" to FUNCTION_CALL_SUPPORTED.size, + "total" to ALL_RESERVED.size + ) + + /** + * ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ์˜ˆ์•ฝ์–ด๋ฅผ ๋ถ„๋ฅ˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param word ๋ถ„๋ฅ˜ํ•  ๋ฌธ์ž์—ด + * @return ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ ๋˜๋Š” null (์˜ˆ์•ฝ์–ด๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ) + */ + fun categorizeFunction(word: String): String? { + val lowerWord = word.lowercase() + return when { + lowerWord in MATH_FUNCTIONS -> "math" + lowerWord in AGGREGATE_FUNCTIONS -> "aggregate" + lowerWord in STRING_FUNCTIONS -> "string" + lowerWord in LOGICAL_KEYWORDS -> "logical" + lowerWord in CONSTANTS -> "constant" + else -> null + } + } + + /** + * ๋ชจ๋“  ์˜ˆ์•ฝ์–ด๋ฅผ ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์˜ˆ์•ฝ์–ด ๋งต + */ + fun getAllReservedByCategory(): Map> = mapOf( + "math" to MATH_FUNCTIONS, + "aggregate" to AGGREGATE_FUNCTIONS, + "string" to STRING_FUNCTIONS, + "logical" to LOGICAL_KEYWORDS, + "constants" to CONSTANTS + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/aggregates/ExpressionEvaluator.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/aggregates/ExpressionEvaluator.kt new file mode 100644 index 00000000..50f0c76e --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/aggregates/ExpressionEvaluator.kt @@ -0,0 +1,677 @@ +package hs.kr.entrydsm.domain.evaluator.aggregates + +// import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.ArgumentsNode +import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode +import hs.kr.entrydsm.domain.ast.entities.BooleanNode +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.entities.IfNode +import hs.kr.entrydsm.domain.ast.entities.NumberNode +import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode +import hs.kr.entrydsm.domain.ast.entities.VariableNode +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import hs.kr.entrydsm.domain.evaluator.registries.FunctionRegistry +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import kotlin.math.E +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.acos +import kotlin.math.acosh +import kotlin.math.asin +import kotlin.math.asinh +import kotlin.math.atan +import kotlin.math.atan2 +import kotlin.math.atanh +import kotlin.math.ceil +import kotlin.math.cos +import kotlin.math.cosh +import kotlin.math.exp +import kotlin.math.floor +import kotlin.math.ln +import kotlin.math.log10 +import kotlin.comparisons.minOf +import kotlin.math.pow +import kotlin.math.round +import kotlin.math.sign +import kotlin.math.sin +import kotlin.math.sinh +import kotlin.math.sqrt +import kotlin.math.tan +import kotlin.math.tanh +import kotlin.math.truncate + +/** + * AST(์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ)๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ ์ˆ˜์‹์„ ํ‰๊ฐ€ํ•˜๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค. + * + * Visitor ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ AST ๋…ธ๋“œ ํƒ€์ž…์— ๋Œ€ํ•œ ํ‰๊ฐ€ ๋กœ์ง์„ ๊ตฌํ˜„ํ•˜๋ฉฐ, + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ, ํ•จ์ˆ˜ ํ˜ธ์ถœ, ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ ๋“ฑ์˜ ๋ชจ๋“  ํ‰๊ฐ€ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ ๋ชจ๋“  ํ‰๊ฐ€ ๋กœ์ง์„ DDD ๊ตฌ์กฐ๋กœ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @property variables ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ๋งต + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Aggregate(context = "evaluator") +class ExpressionEvaluator( + private val variables: Map = emptyMap(), + private val functionRegistry: FunctionRegistry = FunctionRegistry.createDefault() +) : ASTVisitor { + + /** + * ์ฃผ์–ด์ง„ AST ๋…ธ๋“œ๋ฅผ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ํ‰๊ฐ€ํ•  AST ๋…ธ๋“œ + * @return ํ‰๊ฐ€ ๊ฒฐ๊ณผ + * @throws EvaluatorException ํ‰๊ฐ€ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun evaluate(node: ASTNode): Any? { + return try { + node.accept(this) + } catch (e: EvaluatorException) { + throw e + } catch (e: Exception) { + throw EvaluatorException.evaluationFailed(e) + } + } + + /** + * NumberNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ์ˆซ์ž ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitNumber(node: NumberNode): Any? = node.value + + /** + * BooleanNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ๋ถˆ๋ฆฐ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitBoolean(node: BooleanNode): Any? = node.value + + /** + * VariableNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ๋ณ€์ˆ˜ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  VariableNode + * @return ๋ณ€์ˆ˜ ๊ฐ’ + * @throws EvaluatorException ๋ณ€์ˆ˜๊ฐ€ ์ •์˜๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + override fun visitVariable(node: VariableNode): Any? { + return variables[node.name] ?: throw EvaluatorException.undefinedVariable(node.name) + } + + /** + * BinaryOpNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ์ดํ•ญ ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  BinaryOpNode + * @return ์—ฐ์‚ฐ ๊ฒฐ๊ณผ + * @throws EvaluatorException ์—ฐ์‚ฐ ํ‰๊ฐ€ ์‹คํŒจ ์‹œ + */ + override fun visitBinaryOp(node: BinaryOpNode): Any? { + return try { + val left = evaluate(node.left) + val right = evaluate(node.right) + + when (node.operator) { + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž + PLUS -> performArithmeticOp(left, right) { a, b -> a + b } + MINUS -> performArithmeticOp(left, right) { a, b -> a - b } + MULTIPLY -> performArithmeticOp(left, right) { a, b -> a * b } + DIVIDE -> performDivisionOp(left, right) + MODULO -> performArithmeticOp(left, right) { a, b -> a % b } + POWER -> performArithmeticOp(left, right) { a, b -> a.pow(b) } + + // ๋น„๊ต ์—ฐ์‚ฐ์ž + EQUALS -> performComparisonOp(left, right) { a, b -> a == b } + NOT_EQUALS -> performComparisonOp(left, right) { a, b -> a != b } + LESS_THAN -> performComparisonOp(left, right) { a, b -> a < b } + LESS_THAN_OR_EQUAL -> performComparisonOp(left, right) { a, b -> a <= b } + GREATER_THAN -> performComparisonOp(left, right) { a, b -> a > b } + GREATER_THAN_OR_EQUAL -> performComparisonOp(left, right) { a, b -> a >= b } + + // ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž + AND -> performLogicalAnd(left, right) + OR -> performLogicalOr(left, right) + + else -> throw EvaluatorException.unsupportedOperator(node.operator) + } + } catch (e: EvaluatorException) { + throw e + } catch (e: Exception) { + throw EvaluatorException.operatorEvaluationFailed(node.operator, e) + } + } + + /** + * UnaryOpNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ๋‹จํ•ญ ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  UnaryOpNode + * @return ์—ฐ์‚ฐ ๊ฒฐ๊ณผ + * @throws EvaluatorException ์—ฐ์‚ฐ ํ‰๊ฐ€ ์‹คํŒจ ์‹œ + */ + override fun visitUnaryOp(node: UnaryOpNode): Any? { + return try { + val operand = evaluate(node.operand) + + when (node.operator) { + MINUS -> when (operand) { + is Double -> -operand + is Int -> -operand.toDouble() + else -> throw EvaluatorException.unsupportedType(operand?.javaClass?.simpleName ?: "null", operand) + } + PLUS -> when (operand) { + is Double -> operand + is Int -> operand.toDouble() + else -> throw EvaluatorException.unsupportedType(operand?.javaClass?.simpleName ?: "null", operand) + } + NOT -> when (operand) { + is Boolean -> !operand + is Double -> operand == 0.0 + is Int -> operand == 0 + else -> throw EvaluatorException.unsupportedType(operand?.javaClass?.simpleName ?: "null", operand) + } + else -> throw EvaluatorException.unsupportedOperator(node.operator) + } + } catch (e: EvaluatorException) { + throw e + } catch (e: Exception) { + throw EvaluatorException.operatorEvaluationFailed(node.operator, e) + } + } + + /** + * FunctionCallNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * ํ•จ์ˆ˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ๋ฅผ ํ†ตํ•ด ๋ชจ๋“ˆํ™”๋œ ํ•จ์ˆ˜ ํ‰๊ฐ€๊ธฐ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  FunctionCallNode + * @return ํ•จ์ˆ˜ ์‹คํ–‰ ๊ฒฐ๊ณผ + * @throws EvaluatorException ํ•จ์ˆ˜ ์‹คํ–‰ ์‹คํŒจ ์‹œ + */ + override fun visitFunctionCall(node: FunctionCallNode): Any? { + return try { + val args = node.args.map { evaluate(it) } + + // ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์—์„œ ํ•จ์ˆ˜ ํ‰๊ฐ€๊ธฐ ์กฐํšŒ + val evaluator = functionRegistry.get(node.name) + if (evaluator != null) { + return evaluator.evaluate(args) + } + + // ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์— ์—†๋Š” ํŠน์ˆ˜ ํ•จ์ˆ˜๋“ค ์ฒ˜๋ฆฌ (์ƒ์ˆ˜, ๋ณต์žกํ•œ ํ•จ์ˆ˜๋“ค) + when (node.name.uppercase()) { + PI_CONST -> { + validateArgumentCount(node.name, args, 0) + PI + } + E_CONST -> { + validateArgumentCount(node.name, args, 0) + E + } + ASIN -> { + validateArgumentCount(node.name, args, 1) + val value = toDouble(args[0]) + if (value < -1 || value > 1) throw EvaluatorException.mathError("ASIN domain error") + asin(value) + } + ACOS -> { + validateArgumentCount(node.name, args, 1) + val value = toDouble(args[0]) + if (value < -1 || value > 1) throw EvaluatorException.mathError("ACOS domain error") + acos(value) + } + ATAN -> { + validateArgumentCount(node.name, args, 1) + atan(toDouble(args[0])) + } + ATAN2 -> { + validateArgumentCount(node.name, args, 2) + atan2(toDouble(args[0]), toDouble(args[1])) + } + SINH -> { + validateArgumentCount(node.name, args, 1) + sinh(toDouble(args[0])) + } + COSH -> { + validateArgumentCount(node.name, args, 1) + cosh(toDouble(args[0])) + } + TANH -> { + validateArgumentCount(node.name, args, 1) + tanh(toDouble(args[0])) + } + ASINH -> { + validateArgumentCount(node.name, args, 1) + asinh(toDouble(args[0])) + } + ACOSH -> { + validateArgumentCount(node.name, args, 1) + val value = toDouble(args[0]) + if (value < 1) throw EvaluatorException.mathError("ACOSH domain error") + acosh(value) + } + ATANH -> { + validateArgumentCount(node.name, args, 1) + val value = toDouble(args[0]) + if (value <= -1 || value >= 1) throw EvaluatorException.mathError("ATANH domain error") + atanh(value) + } + FLOOR -> { + validateArgumentCount(node.name, args, 1) + floor(toDouble(args[0])) + } + CEIL, CEILING -> { + validateArgumentCount(node.name, args, 1) + ceil(toDouble(args[0])) + } + TRUNCATE, TRUNC -> { + validateArgumentCount(node.name, args, 1) + truncate(toDouble(args[0])) + } + SIGN -> { + validateArgumentCount(node.name, args, 1) + sign(toDouble(args[0])) + } + RANDOM, RAND -> { + validateArgumentCount(node.name, args, 0) + kotlin.random.Random.nextDouble() + } + RADIANS -> { + validateArgumentCount(node.name, args, 1) + toDouble(args[0]) * PI / 180.0 + } + DEGREES -> { + validateArgumentCount(node.name, args, 1) + toDouble(args[0]) * 180.0 / PI + } + MOD -> { + validateArgumentCount(node.name, args, 2) + val dividend = toDouble(args[0]) + val divisor = toDouble(args[1]) + if (divisor == 0.0) throw EvaluatorException.divisionByZero("%") + dividend % divisor + } + GCD -> { + validateArgumentCount(node.name, args, 2) + val a = toDouble(args[0]).toLong() + val b = toDouble(args[1]).toLong() + gcd(a, b).toDouble() + } + LCM -> { + validateArgumentCount(node.name, args, 2) + val a = toDouble(args[0]).toLong() + val b = toDouble(args[1]).toLong() + lcm(a, b).toDouble() + } + FACTORIAL -> { + validateArgumentCount(node.name, args, 1) + val n = toDouble(args[0]).toInt() + if (n < 0) throw EvaluatorException.mathError("FACTORIAL of negative number") + factorial(n).toDouble() + } + COMBINATION, COMB -> { + validateArgumentCount(node.name, args, 2) + val n = toDouble(args[0]).toInt() + val r = toDouble(args[1]).toInt() + if (n < 0 || r < 0 || r > n) throw EvaluatorException.mathError("COMBINATION domain error") + combination(n, r).toDouble() + } + PERMUTATION, PERM -> { + validateArgumentCount(node.name, args, 2) + val n = toDouble(args[0]).toInt() + val r = toDouble(args[1]).toInt() + if (n < 0 || r < 0 || r > n) throw EvaluatorException.mathError("PERMUTATION domain error") + permutation(n, r).toDouble() + } + else -> throw EvaluatorException.unsupportedFunction(node.name) + } + } catch (e: EvaluatorException) { + throw e + } catch (e: Exception) { + throw EvaluatorException.functionExecutionFailed(node.name, e) + } + } + + /** + * IfNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ์กฐ๊ฑด๋ฌธ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitIf(node: IfNode): Any? { + val condition = evaluate(node.condition) + val conditionResult = toBoolean(condition) + + return if (conditionResult) { + evaluate(node.trueValue) + } else { + evaluate(node.falseValue) + } + } + + /** + * ์‚ฐ์ˆ  ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performArithmeticOp(left: Any?, right: Any?, operation: (Double, Double) -> Double): Double { + val leftNum = toDouble(left) + val rightNum = toDouble(right) + return operation(leftNum, rightNum) + } + + /** + * ๋‚˜๋ˆ—์…ˆ ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์™ผ์ชฝ ํ”ผ์—ฐ์‚ฐ์ž + * @param right ์˜ค๋ฅธ์ชฝ ํ”ผ์—ฐ์‚ฐ์ž + * @return ๋‚˜๋ˆ—์…ˆ ๊ฒฐ๊ณผ + * @throws EvaluatorException 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ ๋˜๋Š” ํƒ€์ž… ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ + */ + private fun performDivisionOp(left: Any?, right: Any?): Double { + val leftNum = toDouble(left) + val rightNum = toDouble(right) + + if (rightNum == 0.0) { + throw EvaluatorException.divisionByZero() + } + + return leftNum / rightNum + } + + /** + * ๋น„๊ต ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performComparisonOp(left: Any?, right: Any?, operation: (Double, Double) -> Boolean): Boolean { + val leftNum = toDouble(left) + val rightNum = toDouble(right) + return operation(leftNum, rightNum) + } + + /** + * ๋…ผ๋ฆฌ AND ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performLogicalAnd(left: Any?, right: Any?): Boolean { + val leftBool = toBoolean(left) + if (!leftBool) return false + + val rightBool = toBoolean(right) + return rightBool + } + + /** + * ๋…ผ๋ฆฌ OR ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performLogicalOr(left: Any?, right: Any?): Boolean { + val leftBool = toBoolean(left) + if (leftBool) return true + + val rightBool = toBoolean(right) + return rightBool + } + + /** + * ๊ฐ’์„ Double๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€ํ™˜ํ•  ๊ฐ’ + * @return Double ๊ฐ’ + * @throws EvaluatorException ๋ณ€ํ™˜ ์‹คํŒจ ์‹œ + */ + private fun toDouble(value: Any?): Double { + return when (value) { + is Double -> value + is Int -> value.toDouble() + is Float -> value.toDouble() + is Long -> value.toDouble() + is String -> value.toDoubleOrNull() + ?: throw EvaluatorException.numberConversionError(value) + else -> throw EvaluatorException.numberConversionError(value) + } + } + + /** + * ๊ฐ’์„ Boolean์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun toBoolean(value: Any?): Boolean { + return when (value) { + is Boolean -> value + is Double -> value != 0.0 + is Int -> value != 0 + is Float -> value != 0.0f + is Long -> value != 0L + is String -> value.isNotEmpty() + null -> false + else -> true + } + } + + /** + * ํ•จ์ˆ˜ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param functionName ํ•จ์ˆ˜๋ช… + * @param args ์ธ์ˆ˜ ๋ชฉ๋ก + * @param expectedCount ์˜ˆ์ƒ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @throws EvaluatorException ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ๋งž์ง€ ์•Š๋Š” ๊ฒฝ์šฐ + */ + private fun validateArgumentCount(functionName: String, args: List, expectedCount: Int) { + if (args.size != expectedCount) { + throw EvaluatorException.wrongArgumentCount(functionName, expectedCount, args.size) + } + } + + /** + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์„ ์ถ”๊ฐ€ํ•œ ์ƒˆ๋กœ์šด ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun withVariables(newVariables: Map): ExpressionEvaluator { + return ExpressionEvaluator(variables + newVariables, functionRegistry) + } + + /** + * ๋‹จ์ผ ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•œ ์ƒˆ๋กœ์šด ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun withVariable(name: String, value: Any): ExpressionEvaluator { + return ExpressionEvaluator(variables + (name to value), functionRegistry) + } + + /** + * ํ˜„์žฌ ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getVariables(): Map = variables.toMap() + + /** + * ํŠน์ • ๋ณ€์ˆ˜๊ฐ€ ๋ฐ”์ธ๋”ฉ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun hasVariable(name: String): Boolean = name in variables + + /** + * ๋ฐ”์ธ๋”ฉ๋œ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getVariableCount(): Int = variables.size + + /** + * ์ตœ๋Œ€๊ณต์•ฝ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun gcd(a: Long, b: Long): Long { + return if (b == 0L) a else gcd(b, a % b) + } + + /** + * ์ตœ์†Œ๊ณต๋ฐฐ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun lcm(a: Long, b: Long): Long { + return abs(a * b) / gcd(a, b) + } + + /** + * ํŒฉํ† ๋ฆฌ์–ผ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * Long ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์•ˆ์ „ํ•œ ๋ฒ”์œ„๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. + * + * @param n ํŒฉํ† ๋ฆฌ์–ผ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ + * @return ํŒฉํ† ๋ฆฌ์–ผ ๊ฒฐ๊ณผ + * @throws EvaluatorException ์Œ์ˆ˜์ด๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ํฐ ์ˆ˜์ธ ๊ฒฝ์šฐ + */ + private fun factorial(n: Int): Long { + if (n < 0) throw EvaluatorException.mathError("FACTORIAL of negative number: $n") + if (n > MAX_FACTORIAL_INPUT) { + throw EvaluatorException.mathError("FACTORIAL input too large: $n (max: $MAX_FACTORIAL_INPUT)") + } + if (n <= 1) return 1 + + var result = 1L + for (i in 2..n) { + // ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ์ฒดํฌ + if (result > Long.MAX_VALUE / i) { + throw EvaluatorException.mathError("FACTORIAL overflow detected for input: $n") + } + result *= i + } + return result + } + + /** + * ์กฐํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * Long ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์•ˆ์ „ํ•œ ๋ฒ”์œ„๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. + * + * @param n ์ „์ฒด ๊ฐœ์ˆ˜ + * @param r ์„ ํƒํ•  ๊ฐœ์ˆ˜ + * @return ์กฐํ•ฉ ๊ฒฐ๊ณผ + * @throws EvaluatorException ์Œ์ˆ˜์ด๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ํฐ ์ˆ˜์ธ ๊ฒฝ์šฐ + */ + private fun combination(n: Int, r: Int): Long { + if (n < 0 || r < 0) throw EvaluatorException.mathError("COMBINATION with negative inputs: n=$n, r=$r") + if (r > n) return 0 + if (r == 0 || r == n) return 1 + + // ์ž…๋ ฅ ํฌ๊ธฐ ๊ฒ€์ฆ - ์กฐํ•ฉ์ด ํŒฉํ† ๋ฆฌ์–ผ๋ณด๋‹ค ์ž‘์œผ๋ฏ€๋กœ ๋” ํฐ ๊ฐ’ ํ—ˆ์šฉ + if (n > MAX_COMBINATION_INPUT) { + throw EvaluatorException.mathError("COMBINATION input too large: n=$n (max: $MAX_COMBINATION_INPUT)") + } + + val k = minOf(r, n - r) + var result = 1L + + for (i in 0 until k) { + val numerator = n - i + val denominator = i + 1 + + // ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ์ฒดํฌ - ๊ณฑ์…ˆ ์ „์— ๊ฒ€์‚ฌ + if (result > Long.MAX_VALUE / numerator) { + throw EvaluatorException.mathError("COMBINATION overflow detected: n=$n, r=$r") + } + + result = result * numerator / denominator + } + + return result + } + + /** + * ์ˆœ์—ด์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * Long ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•ด ์•ˆ์ „ํ•œ ๋ฒ”์œ„๋กœ ์ œํ•œํ•ฉ๋‹ˆ๋‹ค. + * + * @param n ์ „์ฒด ๊ฐœ์ˆ˜ + * @param r ์„ ํƒํ•  ๊ฐœ์ˆ˜ + * @return ์ˆœ์—ด ๊ฒฐ๊ณผ + * @throws EvaluatorException ์Œ์ˆ˜์ด๊ฑฐ๋‚˜ ๋„ˆ๋ฌด ํฐ ์ˆ˜์ธ ๊ฒฝ์šฐ + */ + private fun permutation(n: Int, r: Int): Long { + if (n < 0 || r < 0) throw EvaluatorException.mathError("PERMUTATION with negative inputs: n=$n, r=$r") + if (r > n) return 0 + if (r == 0) return 1 + + // ์ž…๋ ฅ ํฌ๊ธฐ ๊ฒ€์ฆ - ์ˆœ์—ด์€ ํŒฉํ† ๋ฆฌ์–ผ๊ณผ ์œ ์‚ฌํ•œ ์„ฑ์žฅ๋ฅ  + if (n > MAX_PERMUTATION_INPUT || r > MAX_PERMUTATION_INPUT) { + throw EvaluatorException.mathError("PERMUTATION input too large: n=$n, r=$r (max: $MAX_PERMUTATION_INPUT)") + } + + var result = 1L + for (i in 0 until r) { + val factor = n - i + + // ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ์ฒดํฌ + if (result > Long.MAX_VALUE / factor) { + throw EvaluatorException.mathError("PERMUTATION overflow detected: n=$n, r=$r") + } + + result *= factor + } + + return result + } + + /** + * ์ธ์ˆ˜ ๋…ธ๋“œ๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ์ธ์ˆ˜ ๋…ธ๋“œ + * @return ํ‰๊ฐ€๋œ ์ธ์ˆ˜ ๋ฆฌ์ŠคํŠธ + */ + override fun visitArguments(node: ArgumentsNode): Any? { + return node.arguments.map { it.accept(this) } + } + + companion object { + // Operators + private const val PLUS = "+" + private const val MINUS = "-" + private const val MULTIPLY = "*" + private const val DIVIDE = "/" + private const val MODULO = "%" + private const val POWER = "^" + private const val EQUALS = "==" + private const val NOT_EQUALS = "!=" + private const val LESS_THAN = "<" + private const val LESS_THAN_OR_EQUAL = "<=" + private const val GREATER_THAN = ">" + private const val GREATER_THAN_OR_EQUAL = ">=" + private const val AND = "&&" + private const val OR = "||" + private const val NOT = "!" + + // Functions + private const val PI_CONST = "PI" + private const val E_CONST = "E" + private const val ASIN = "ASIN" + private const val ACOS = "ACOS" + private const val ATAN = "ATAN" + private const val ATAN2 = "ATAN2" + private const val SINH = "SINH" + private const val COSH = "COSH" + private const val TANH = "TANH" + private const val ASINH = "ASINH" + private const val ACOSH = "ACOSH" + private const val ATANH = "ATANH" + private const val FLOOR = "FLOOR" + private const val CEIL = "CEIL" + private const val CEILING = "CEILING" + private const val TRUNCATE = "TRUNCATE" + private const val TRUNC = "TRUNC" + private const val SIGN = "SIGN" + private const val RANDOM = "RANDOM" + private const val RAND = "RAND" + private const val RADIANS = "RADIANS" + private const val DEGREES = "DEGREES" + private const val MOD = "MOD" + private const val GCD = "GCD" + private const val LCM = "LCM" + private const val FACTORIAL = "FACTORIAL" + private const val COMBINATION = "COMBINATION" + private const val COMB = "COMB" + private const val PERMUTATION = "PERMUTATION" + private const val PERM = "PERM" + + // Long ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๋ฐฉ์ง€๋ฅผ ์œ„ํ•œ ์•ˆ์ „ํ•œ ์ž…๋ ฅ ํฌ๊ธฐ ์ œํ•œ + private const val MAX_FACTORIAL_INPUT = 20 // 20! = 2,432,902,008,176,640,000 (Long ๋ฒ”์œ„ ๋‚ด) + private const val MAX_COMBINATION_INPUT = 62 // C(62,31)์ด Long ๋ฒ”์œ„ ๋‚ด ์ตœ๋Œ€๊ฐ’ + private const val MAX_PERMUTATION_INPUT = 20 // P(20,20) = 20!๊ณผ ๋™์ผ + + /** + * ๋นˆ ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์œผ๋กœ ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun create(): ExpressionEvaluator = ExpressionEvaluator() + + /** + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ๊ณผ ํ•จ๊ป˜ ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun create(variables: Map): ExpressionEvaluator = ExpressionEvaluator(variables) + + /** + * ์ปค์Šคํ…€ ํ•จ์ˆ˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ์™€ ํ•จ๊ป˜ ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun create(variables: Map, functionRegistry: FunctionRegistry): ExpressionEvaluator = + ExpressionEvaluator(variables, functionRegistry) + } +} diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/entities/EvaluationContext.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/entities/EvaluationContext.kt new file mode 100644 index 00000000..cdba4170 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/entities/EvaluationContext.kt @@ -0,0 +1,297 @@ +package hs.kr.entrydsm.domain.evaluator.entities + +// Removed VariableBinding import for simplicity +import hs.kr.entrydsm.global.annotation.entities.Entity +import java.time.Instant + +/** + * ํ‘œํ˜„์‹ ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. + * + * DDD Entity ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ‰๊ฐ€ ๊ณผ์ •์—์„œ ํ•„์š”ํ•œ ์ƒํƒœ์™€ ์„ค์ •์„ + * ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ, ํ•จ์ˆ˜ ๋“ฑ๋ก, ํ‰๊ฐ€ ์„ค์ • ๋“ฑ์„ ๊ด€๋ฆฌํ•˜๋ฉฐ + * ํ‰๊ฐ€ ์„ธ์…˜์˜ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @property id ์ปจํ…์ŠคํŠธ ๊ณ ์œ  ์‹๋ณ„์ž + * @property variables ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ + * @property createdAt ์ƒ์„ฑ ์‹œ๊ฐ„ + * @property lastModified ๋งˆ์ง€๋ง‰ ์ˆ˜์ • ์‹œ๊ฐ„ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Entity( + aggregateRoot = hs.kr.entrydsm.domain.evaluator.aggregates.ExpressionEvaluator::class, + context = "evaluator" +) +data class EvaluationContext( + val id: String, + val variables: Map = emptyMap(), + val createdAt: Instant = Instant.now(), + val lastModified: Instant = Instant.now(), + val maxDepth: Int = 100, + val maxVariables: Int = 1000, + val enableOptimization: Boolean = true, + val enableCaching: Boolean = true, + val strictMode: Boolean = false, + val metadata: Map = emptyMap() +) { + + /** + * ๋ณ€์ˆ˜๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @param value ๋ณ€์ˆ˜ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun addVariable(name: String, value: Any): EvaluationContext { + if (name.isBlank()) { + throw hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException.invalidVariableName(name) + } + if (variables.size >= maxVariables) { + throw hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException.evaluationError( + RuntimeException("์ตœ๋Œ€ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $maxVariables") + ) + } + + return copy( + variables = variables + (name to value), + lastModified = Instant.now() + ) + } + + /** + * ์—ฌ๋Ÿฌ ๋ณ€์ˆ˜๋ฅผ ์ผ๊ด„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newVariables ์ถ”๊ฐ€ํ•  ๋ณ€์ˆ˜ ๋งต + * @return ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun addVariables(newVariables: Map): EvaluationContext { + if (variables.size + newVariables.size > maxVariables) { + throw hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException.evaluationError( + RuntimeException("์ตœ๋Œ€ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $maxVariables") + ) + } + + return copy( + variables = variables + newVariables, + lastModified = Instant.now() + ) + } + + /** + * ๋ณ€์ˆ˜๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์ œ๊ฑฐํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun removeVariable(name: String): EvaluationContext { + return copy( + variables = variables - name, + lastModified = Instant.now() + ) + } + + /** + * ๋ณ€์ˆ˜๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ™•์ธํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ์กด์žฌํ•˜๋ฉด true + */ + fun hasVariable(name: String): Boolean { + return variables.containsKey(name) + } + + /** + * ๋ณ€์ˆ˜ ๊ฐ’์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์กฐํšŒํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ๋ณ€์ˆ˜ ๊ฐ’ (์—†์œผ๋ฉด null) + */ + fun getVariable(name: String): Any? { + return variables[name] + } + + /** + * ๋ชจ๋“  ๋ณ€์ˆ˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€์ˆ˜ ์ด๋ฆ„ ์ง‘ํ•ฉ + */ + fun getVariableNames(): Set { + return variables.keys + } + + /** + * ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ + */ + fun getVariableCount(): Int { + return variables.size + } + + /** + * ์ปจํ…์ŠคํŠธ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ดˆ๊ธฐํ™”๋œ ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun clear(): EvaluationContext { + return copy( + variables = emptyMap(), + lastModified = Instant.now() + ) + } + + /** + * ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ‚ค + * @param value ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun withMetadata(key: String, value: Any): EvaluationContext { + return copy( + metadata = metadata + (key to value), + lastModified = Instant.now() + ) + } + + /** + * ์—ฌ๋Ÿฌ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ผ๊ด„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newMetadata ์ถ”๊ฐ€ํ•  ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋งต + * @return ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun withMetadata(newMetadata: Map): EvaluationContext { + return copy( + metadata = metadata + newMetadata, + lastModified = Instant.now() + ) + } + + /** + * ์ตœ๋Œ€ ๊นŠ์ด๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param depth ์ตœ๋Œ€ ๊นŠ์ด + * @return ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun withMaxDepth(depth: Int): EvaluationContext { + if (depth <= 0) { + throw hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException.evaluationError( + IllegalArgumentException("์ตœ๋Œ€ ๊นŠ์ด๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค: $depth") + ) + } + return copy( + maxDepth = depth, + lastModified = Instant.now() + ) + } + + /** + * ์—„๊ฒฉ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param strict ์—„๊ฒฉ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun withStrictMode(strict: Boolean): EvaluationContext { + return copy( + strictMode = strict, + lastModified = Instant.now() + ) + } + + /** + * ์ตœ์ ํ™” ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ์ตœ์ ํ™” ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun withOptimization(enabled: Boolean): EvaluationContext { + return copy( + enableOptimization = enabled, + lastModified = Instant.now() + ) + } + + /** + * ์บ์‹ฑ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ์บ์‹ฑ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ + */ + fun withCaching(enabled: Boolean): EvaluationContext { + return copy( + enableCaching = enabled, + lastModified = Instant.now() + ) + } + + /** + * ์ปจํ…์ŠคํŠธ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isValid(): Boolean { + return id.isNotBlank() && + maxDepth > 0 && + maxVariables > 0 && + variables.size <= maxVariables + } + + /** + * ์ปจํ…์ŠคํŠธ์˜ ์š”์•ฝ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์š”์•ฝ ์ •๋ณด ๋งต + */ + fun getSummary(): Map = mapOf( + "id" to id, + "variableCount" to getVariableCount(), + "maxDepth" to maxDepth, + "maxVariables" to maxVariables, + "enableOptimization" to enableOptimization, + "enableCaching" to enableCaching, + "strictMode" to strictMode, + "createdAt" to createdAt, + "lastModified" to lastModified, + "isValid" to isValid() + ) + + companion object { + /** + * ๊ธฐ๋ณธ ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ปจํ…์ŠคํŠธ ID + * @return ๊ธฐ๋ณธ ์ปจํ…์ŠคํŠธ + */ + fun create(id: String): EvaluationContext { + if (id.isBlank()) { + throw hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException.invalidVariableName(id) + } + return EvaluationContext( + id = id, + variables = emptyMap() + ) + } + + /** + * ๋ณ€์ˆ˜์™€ ํ•จ๊ป˜ ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ปจํ…์ŠคํŠธ ID + * @param variables ์ดˆ๊ธฐ ๋ณ€์ˆ˜ ๋งต + * @return ์ปจํ…์ŠคํŠธ + */ + fun create(id: String, variables: Map): EvaluationContext { + return create(id).addVariables(variables) + } + + /** + * ๋นˆ ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋นˆ ์ปจํ…์ŠคํŠธ + */ + fun empty(): EvaluationContext { + return create("empty") + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/entities/MathFunction.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/entities/MathFunction.kt new file mode 100644 index 00000000..4ed40dd4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/entities/MathFunction.kt @@ -0,0 +1,317 @@ +package hs.kr.entrydsm.domain.evaluator.entities + +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import hs.kr.entrydsm.global.annotation.entities.Entity +import java.time.Instant + +/** + * ์ˆ˜ํ•™ ํ•จ์ˆ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. + * + * DDD Entity ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์ˆ˜ํ•™ ํ•จ์ˆ˜์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์™€ ์‹คํ–‰ ์ •๋ณด๋ฅผ + * ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ํ•จ์ˆ˜ ์ด๋ฆ„, ์ธ์ˆ˜ ๊ฐœ์ˆ˜, ๋„๋ฉ”์ธ ์ œ์•ฝ์‚ฌํ•ญ ๋“ฑ์„ ๊ด€๋ฆฌํ•˜๋ฉฐ + * ํ•จ์ˆ˜ ํ˜ธ์ถœ์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ๊ณผ ์‹คํ–‰์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. + * + * @property name ํ•จ์ˆ˜ ์ด๋ฆ„ + * @property minArguments ์ตœ์†Œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @property maxArguments ์ตœ๋Œ€ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @property description ํ•จ์ˆ˜ ์„ค๋ช… + * @property category ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ + * @property implementation ํ•จ์ˆ˜ ๊ตฌํ˜„์ฒด + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Entity( + aggregateRoot = hs.kr.entrydsm.domain.evaluator.aggregates.ExpressionEvaluator::class, + context = "evaluator" +) +data class MathFunction( + val name: String, + val minArguments: Int, + val maxArguments: Int, + val description: String, + val category: FunctionCategory, + val implementation: (List) -> Any, + val createdAt: Instant = Instant.now(), + val metadata: Map = emptyMap() +) { + + init { + if (name.isBlank()) throw EvaluatorException.invalidVariableName("ํ•จ์ˆ˜ ์ด๋ฆ„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + if (minArguments < 0) throw EvaluatorException.wrongArgumentCount("minArguments", 0, minArguments) + if (maxArguments < minArguments) throw EvaluatorException.wrongArgumentCount("maxArguments", minArguments, maxArguments) + if (description.isBlank()) throw EvaluatorException.invalidVariableName("ํ•จ์ˆ˜ ์„ค๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + } + + /** + * ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class FunctionCategory(val displayName: String, val description: String) { + ARITHMETIC("์‚ฐ์ˆ ", "๊ธฐ๋ณธ ์‚ฐ์ˆ  ์—ฐ์‚ฐ ํ•จ์ˆ˜"), + TRIGONOMETRIC("์‚ผ๊ฐ", "์‚ผ๊ฐ ํ•จ์ˆ˜"), + HYPERBOLIC("์Œ๊ณก", "์Œ๊ณก ํ•จ์ˆ˜"), + LOGARITHMIC("๋กœ๊ทธ", "๋กœ๊ทธ ๋ฐ ์ง€์ˆ˜ ํ•จ์ˆ˜"), + STATISTICAL("ํ†ต๊ณ„", "ํ†ต๊ณ„ ๋ฐ ์ง‘๊ณ„ ํ•จ์ˆ˜"), + LOGICAL("๋…ผ๋ฆฌ", "๋…ผ๋ฆฌ ์—ฐ์‚ฐ ํ•จ์ˆ˜"), + COMPARISON("๋น„๊ต", "๋น„๊ต ์—ฐ์‚ฐ ํ•จ์ˆ˜"), + CONVERSION("๋ณ€ํ™˜", "ํƒ€์ž… ๋ฐ ๋‹จ์œ„ ๋ณ€ํ™˜ ํ•จ์ˆ˜"), + UTILITY("์œ ํ‹ธ๋ฆฌํ‹ฐ", "๊ธฐํƒ€ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜"), + CUSTOM("์‚ฌ์šฉ์ž์ •์˜", "์‚ฌ์šฉ์ž ์ •์˜ ํ•จ์ˆ˜"); + + override fun toString(): String = displayName + } + + /** + * ์ฃผ์–ด์ง„ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param argumentCount ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isValidArgumentCount(argumentCount: Int): Boolean { + return argumentCount in minArguments..maxArguments + } + + /** + * ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param arguments ์ธ์ˆ˜ ๋ชฉ๋ก + * @throws EvaluatorException ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateArgumentCount(arguments: List) { + if (!isValidArgumentCount(arguments.size)) { + throw EvaluatorException.wrongArgumentCount(name, minArguments, arguments.size) + } + } + + /** + * ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param arguments ์ธ์ˆ˜ ๋ชฉ๋ก + * @return ์‹คํ–‰ ๊ฒฐ๊ณผ + * @throws EvaluatorException ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š๊ฑฐ๋‚˜ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun execute(arguments: List): Any { + validateArgumentCount(arguments) + return try { + implementation(arguments) + } catch (e: EvaluatorException) { + throw e + } catch (e: Exception) { + throw EvaluatorException.functionExecutionFailed(name, e) + } + } + + /** + * ๊ฐ€๋ณ€ ์ธ์ˆ˜๋กœ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param arguments ๊ฐ€๋ณ€ ์ธ์ˆ˜ + * @return ์‹คํ–‰ ๊ฒฐ๊ณผ + */ + fun execute(vararg arguments: Any): Any { + return execute(arguments.toList()) + } + + /** + * ํ•จ์ˆ˜๊ฐ€ ๊ณ ์ • ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณ ์ • ์ธ์ˆ˜ ๊ฐœ์ˆ˜์ด๋ฉด true + */ + fun hasFixedArgumentCount(): Boolean { + return minArguments == maxArguments + } + + /** + * ํ•จ์ˆ˜๊ฐ€ ๊ฐ€๋ณ€ ์ธ์ˆ˜๋ฅผ ๋ฐ›๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ€๋ณ€ ์ธ์ˆ˜์ด๋ฉด true + */ + fun isVariadic(): Boolean { + return !hasFixedArgumentCount() + } + + /** + * ํ•จ์ˆ˜์˜ ์ธ์ˆ˜ ๋ฒ”์œ„๋ฅผ ๋ฌธ์ž์—ด๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์ˆ˜ ๋ฒ”์œ„ ๋ฌธ์ž์—ด + */ + fun getArgumentRangeString(): String { + return if (hasFixedArgumentCount()) { + minArguments.toString() + } else { + "$minArguments-$maxArguments" + } + } + + /** + * ํ•จ์ˆ˜์˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•จ์ˆ˜ ์‹œ๊ทธ๋‹ˆ์ฒ˜ + */ + fun getSignature(): String { + val argRange = getArgumentRangeString() + return "$name($argRange args)" + } + + /** + * ํ•จ์ˆ˜์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ์ •๋ณด ๋งต + */ + fun getDetails(): Map = mapOf( + "name" to name, + "signature" to getSignature(), + "description" to description, + "category" to category.displayName, + "minArguments" to minArguments, + "maxArguments" to maxArguments, + "hasFixedArgumentCount" to hasFixedArgumentCount(), + "isVariadic" to isVariadic(), + "createdAt" to createdAt, + "metadata" to metadata + ) + + /** + * ํ•จ์ˆ˜์˜ ์‚ฌ์šฉ๋ฒ• ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ฌ์šฉ๋ฒ• ๋ฌธ์ž์—ด + */ + fun getUsage(): String = buildString { + appendLine("ํ•จ์ˆ˜: $name") + appendLine("์นดํ…Œ๊ณ ๋ฆฌ: ${category.displayName}") + appendLine("์„ค๋ช…: $description") + appendLine("์ธ์ˆ˜: ${getArgumentRangeString()}๊ฐœ") + appendLine("์‚ฌ์šฉ๋ฒ•: ${getSignature()}") + if (metadata.isNotEmpty()) { + appendLine("์ถ”๊ฐ€ ์ •๋ณด:") + metadata.forEach { (key, value) -> + appendLine(" $key: $value") + } + } + } + + override fun toString(): String { + return "MathFunction(name='$name', category=${category.displayName}, args=${getArgumentRangeString()})" + } + + companion object { + /** + * ๊ฐ€๋ณ€ ์ธ์ˆ˜ ํ•จ์ˆ˜์˜ ์‹ค์šฉ์  ์ตœ๋Œ€ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ ์ œํ•œ + * ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰๊ณผ ์„ฑ๋Šฅ์„ ๊ณ ๋ คํ•œ ํ•ฉ๋ฆฌ์ ์ธ ์ƒํ•œ์„  + */ + private const val MAX_PRACTICAL_ARGUMENTS = 1000 + + /** + * ๊ณ ์ • ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๊ฐ€์ง„ ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜ ์ด๋ฆ„ + * @param argumentCount ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param description ํ•จ์ˆ˜ ์„ค๋ช… + * @param category ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ + * @param implementation ํ•จ์ˆ˜ ๊ตฌํ˜„์ฒด + * @return MathFunction ์ธ์Šคํ„ด์Šค + */ + fun fixedArgs( + name: String, + argumentCount: Int, + description: String, + category: FunctionCategory, + implementation: (List) -> Any + ): MathFunction { + return MathFunction( + name = name, + minArguments = argumentCount, + maxArguments = argumentCount, + description = description, + category = category, + implementation = implementation + ) + } + + /** + * ๊ฐ€๋ณ€ ์ธ์ˆ˜๋ฅผ ๊ฐ€์ง„ ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜ ์ด๋ฆ„ + * @param minArgs ์ตœ์†Œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param maxArgs ์ตœ๋Œ€ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param description ํ•จ์ˆ˜ ์„ค๋ช… + * @param category ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ + * @param implementation ํ•จ์ˆ˜ ๊ตฌํ˜„์ฒด + * @return MathFunction ์ธ์Šคํ„ด์Šค + */ + fun varArgs( + name: String, + minArgs: Int, + maxArgs: Int, + description: String, + category: FunctionCategory, + implementation: (List) -> Any + ): MathFunction { + return MathFunction( + name = name, + minArguments = minArgs, + maxArguments = maxArgs, + description = description, + category = category, + implementation = implementation + ) + } + + /** + * ์ธ์ˆ˜ ๊ฐœ์ˆ˜ ์ œํ•œ์ด ์œ ์—ฐํ•œ ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๋ฉ”๋ชจ๋ฆฌ์™€ ์„ฑ๋Šฅ์„ ๊ณ ๋ คํ•˜์—ฌ ์‹ค์šฉ์ ์ธ ์ƒํ•œ์„ ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜ ์ด๋ฆ„ + * @param description ํ•จ์ˆ˜ ์„ค๋ช… + * @param category ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ + * @param implementation ํ•จ์ˆ˜ ๊ตฌํ˜„์ฒด + * @return MathFunction ์ธ์Šคํ„ด์Šค + */ + fun unlimited( + name: String, + description: String, + category: FunctionCategory, + implementation: (List) -> Any + ): MathFunction { + return MathFunction( + name = name, + minArguments = 0, + maxArguments = MAX_PRACTICAL_ARGUMENTS, + description = description, + category = category, + implementation = implementation + ) + } + + /** + * ์ปค์Šคํ…€ ์ตœ๋Œ€ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋กœ ๊ฐ€๋ณ€ ์ธ์ˆ˜ ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜ ์ด๋ฆ„ + * @param minArgs ์ตœ์†Œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param maxArgs ์ตœ๋Œ€ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ (MAX_PRACTICAL_ARGUMENTS๋กœ ์ œํ•œ๋จ) + * @param description ํ•จ์ˆ˜ ์„ค๋ช… + * @param category ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ + * @param implementation ํ•จ์ˆ˜ ๊ตฌํ˜„์ฒด + * @return MathFunction ์ธ์Šคํ„ด์Šค + */ + fun flexibleArgs( + name: String, + minArgs: Int, + maxArgs: Int, + description: String, + category: FunctionCategory, + implementation: (List) -> Any + ): MathFunction { + val safeMaxArgs = minOf(maxArgs, MAX_PRACTICAL_ARGUMENTS) + return MathFunction( + name = name, + minArguments = minArgs, + maxArguments = safeMaxArgs, + description = description, + category = category, + implementation = implementation + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/exceptions/EvaluatorException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/exceptions/EvaluatorException.kt new file mode 100644 index 00000000..b2a1d211 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/exceptions/EvaluatorException.kt @@ -0,0 +1,350 @@ +package hs.kr.entrydsm.domain.evaluator.exceptions + +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * Evaluator ๋„๋ฉ”์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ํ‘œํ˜„์‹ ํ‰๊ฐ€, ์—ฐ์‚ฐ์ž ์ฒ˜๋ฆฌ, ํ•จ์ˆ˜ ํ˜ธ์ถœ, ๋ณ€์ˆ˜ ํ•ด์„, ํƒ€์ž… ๋ณ€ํ™˜ ๋“ฑ์˜ + * ํ‰๊ฐ€ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @property operator ์˜ค๋ฅ˜์™€ ๊ด€๋ จ๋œ ์—ฐ์‚ฐ์ž (์„ ํƒ์‚ฌํ•ญ) + * @property function ์˜ค๋ฅ˜์™€ ๊ด€๋ จ๋œ ํ•จ์ˆ˜๋ช… (์„ ํƒ์‚ฌํ•ญ) + * @property variable ์˜ค๋ฅ˜์™€ ๊ด€๋ จ๋œ ๋ณ€์ˆ˜๋ช… (์„ ํƒ์‚ฌํ•ญ) + * @property expectedArgCount ์˜ˆ์ƒ๋œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ (์„ ํƒ์‚ฌํ•ญ) + * @property actualArgCount ์‹ค์ œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ (์„ ํƒ์‚ฌํ•ญ) + * @property valueType ๊ฐ’์˜ ํƒ€์ž… (์„ ํƒ์‚ฌํ•ญ) + * @property value ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ ๊ฐ’ (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +class EvaluatorException( + errorCode: ErrorCode, + val operator: String? = null, + val function: String? = null, + val variable: String? = null, + val expectedArgCount: Int? = null, + val actualArgCount: Int? = null, + val valueType: String? = null, + val value: Any? = null, + message: String = buildEvaluatorMessage(errorCode, operator, function, variable, expectedArgCount, actualArgCount, valueType, value), + cause: Throwable? = null +) : DomainException(errorCode, message, cause) { + + companion object { + /** + * Evaluator ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param errorCode ์˜ค๋ฅ˜ ์ฝ”๋“œ + * @param operator ์—ฐ์‚ฐ์ž + * @param function ํ•จ์ˆ˜๋ช… + * @param variable ๋ณ€์ˆ˜๋ช… + * @param expectedArgCount ์˜ˆ์ƒ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param actualArgCount ์‹ค์ œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param valueType ๊ฐ’ ํƒ€์ž… + * @param value ๊ฐ’ + * @return ๊ตฌ์„ฑ๋œ ๋ฉ”์‹œ์ง€ + */ + private fun buildEvaluatorMessage( + errorCode: ErrorCode, + operator: String?, + function: String?, + variable: String?, + expectedArgCount: Int?, + actualArgCount: Int?, + valueType: String?, + value: Any? + ): String { + val baseMessage = errorCode.description + val details = mutableListOf() + + operator?.let { details.add("์—ฐ์‚ฐ์ž: $it") } + function?.let { details.add("ํ•จ์ˆ˜: $it") } + variable?.let { details.add("๋ณ€์ˆ˜: $it") } + if (expectedArgCount != null && actualArgCount != null) { + details.add("์ธ์ˆ˜: $actualArgCount (์˜ˆ์ƒ: $expectedArgCount)") + } + valueType?.let { details.add("ํƒ€์ž…: $it") } + value?.let { details.add("๊ฐ’: $it") } + + return if (details.isNotEmpty()) { + "$baseMessage (${details.joinToString(", ")})" + } else { + baseMessage + } + } + + /** + * ํ‰๊ฐ€ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun evaluationError(cause: Throwable? = null): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.EVALUATION_ERROR, + cause = cause + ) + } + + /** + * 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ๋‚˜๋ˆ„๊ธฐ ์—ฐ์‚ฐ์ž + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun divisionByZero(operator: String = "/"): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.DIVISION_BY_ZERO, + operator = operator + ) + } + + /** + * ์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param variable ์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช… + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun undefinedVariable(variable: String): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.UNDEFINED_VARIABLE, + variable = variable + ) + } + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedOperator(operator: String): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.UNSUPPORTED_OPERATOR, + operator = operator + ) + } + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ•จ์ˆ˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param function ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ•จ์ˆ˜๋ช… + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedFunction(function: String): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.UNSUPPORTED_FUNCTION, + function = function + ) + } + + /** + * ์ž˜๋ชป๋œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param function ํ•จ์ˆ˜๋ช… + * @param expectedCount ์˜ˆ์ƒ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param actualCount ์‹ค์ œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun wrongArgumentCount(function: String, expectedCount: Int, actualCount: Int): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.WRONG_ARGUMENT_COUNT, + function = function, + expectedArgCount = expectedCount, + actualArgCount = actualCount + ) + } + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param valueType ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํƒ€์ž… + * @param value ํ•ด๋‹น ๊ฐ’ + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedType(valueType: String, value: Any? = null): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.UNSUPPORTED_TYPE, + valueType = valueType, + value = value + ) + } + + /** + * ์ˆซ์ž ๋ณ€ํ™˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€ํ™˜ ์‹คํŒจํ•œ ๊ฐ’ + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun numberConversionError(value: Any?, cause: Throwable? = null): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.NUMBER_CONVERSION_ERROR, + value = value, + valueType = value?.javaClass?.simpleName, + cause = cause + ) + } + + /** + * ์ˆ˜ํ•™ ์—ฐ์‚ฐ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param message ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun mathError(message: String, cause: Throwable? = null): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.MATH_ERROR, + cause = cause, + message = message + ) + } + + /** + * ํ‰๊ฐ€ ์‹คํŒจ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun evaluationFailed(cause: Throwable? = null): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.EVALUATION_FAILED, + cause = cause + ) + } + + /** + * ์—ฐ์‚ฐ์ž ํ‰๊ฐ€ ์‹คํŒจ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์‹คํŒจํ•œ ์—ฐ์‚ฐ์ž + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun operatorEvaluationFailed(operator: String, cause: Throwable? = null): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.OPERATOR_EVALUATION_FAILED, + operator = operator, + cause = cause + ) + } + + /** + * ํ•จ์ˆ˜ ์‹คํ–‰ ์‹คํŒจ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param function ์‹คํŒจํ•œ ํ•จ์ˆ˜๋ช… + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun functionExecutionFailed(function: String, cause: Throwable? = null): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.FUNCTION_EXECUTION_FAILED, + function = function, + cause = cause + ) + } + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param variable ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช… + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun invalidVariableName(variable: String): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.VARIABLE_NAME_INVALID, + variable = variable + ) + } + + /** + * ํ‰๊ฐ€ ๊นŠ์ด ์ดˆ๊ณผ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxDepth ์ตœ๋Œ€ ํ—ˆ์šฉ ๊นŠ์ด + * @param actualDepth ์‹ค์ œ ๊นŠ์ด + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun evaluationDepthExceeded(maxDepth: Int, actualDepth: Int): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.EVALUATION_DEPTH_EXCEEDED, + message = "ํ‰๊ฐ€ ๊นŠ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค (์ตœ๋Œ€: $maxDepth, ์‹ค์ œ: $actualDepth)" + ) + } + + /** + * ํ‰๊ฐ€ ๋ณต์žก๋„ ์ดˆ๊ณผ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxComplexity ์ตœ๋Œ€ ํ—ˆ์šฉ ๋ณต์žก๋„ + * @param actualComplexity ์‹ค์ œ ๋ณต์žก๋„ + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun evaluationComplexityExceeded(maxComplexity: Int, actualComplexity: Int): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.EVALUATION_COMPLEXITY_EXCEEDED, + message = "ํ‰๊ฐ€ ๋ณต์žก๋„๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค (์ตœ๋Œ€: $maxComplexity, ์‹ค์ œ: $actualComplexity)" + ) + } + + /** + * ๋ณด์•ˆ ์ •์ฑ… ์œ„๋ฐ˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param reason ์œ„๋ฐ˜ ์‚ฌ์œ  + * @return EvaluatorException ์ธ์Šคํ„ด์Šค + */ + fun securityViolation(reason: String): EvaluatorException { + return EvaluatorException( + errorCode = ErrorCode.EVALUATION_SECURITY_VIOLATION, + message = "๋ณด์•ˆ ์ •์ฑ… ์œ„๋ฐ˜: $reason" + ) + } + } + + /** + * Evaluator ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฐ์‚ฐ์ž, ํ•จ์ˆ˜, ๋ณ€์ˆ˜, ์ธ์ˆ˜, ํƒ€์ž…, ๊ฐ’ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋งต + */ + fun getEvaluatorInfo(): Map { + val info = mutableMapOf() + + operator?.let { info["operator"] = it } + function?.let { info["function"] = it } + variable?.let { info["variable"] = it } + expectedArgCount?.let { info["expectedArgCount"] = it } + actualArgCount?.let { info["actualArgCount"] = it } + valueType?.let { info["valueType"] = it } + value?.let { info["value"] = it } + + return info + } + + /** + * ์ „์ฒด ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ์ •๋ณด์™€ Evaluator ์ •๋ณด๊ฐ€ ๊ฒฐํ•ฉ๋œ ๋งต + */ + fun toCompleteErrorInfo(): Map { + val baseInfo = super.toErrorInfo().toMutableMap() + val evaluatorInfo = getEvaluatorInfo() + + evaluatorInfo.forEach { (key, value) -> + baseInfo[key] = value?.toString() ?: "" + } + + return baseInfo + } + + override fun toString(): String { + val evaluatorDetails = getEvaluatorInfo() + return if (evaluatorDetails.isNotEmpty()) { + "${super.toString()}, evaluator=${evaluatorDetails}" + } else { + super.toString() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/factories/EvaluatorFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/factories/EvaluatorFactory.kt new file mode 100644 index 00000000..5f8561fe --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/factories/EvaluatorFactory.kt @@ -0,0 +1,308 @@ +package hs.kr.entrydsm.domain.evaluator.factories + +import hs.kr.entrydsm.domain.evaluator.aggregates.ExpressionEvaluator +import hs.kr.entrydsm.domain.evaluator.services.MathFunctionService +import hs.kr.entrydsm.domain.evaluator.values.VariableBinding +import hs.kr.entrydsm.domain.factories.EnvironmentFactory +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.configuration.ASTConfiguration +import hs.kr.entrydsm.global.configuration.CalculatorConfiguration +import hs.kr.entrydsm.global.configuration.EvaluatorConfiguration +import hs.kr.entrydsm.global.configuration.ExpresserConfiguration +import hs.kr.entrydsm.global.configuration.LexerConfiguration +import hs.kr.entrydsm.global.configuration.ParserConfiguration +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationChangeListener +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider + +/** + * Evaluator ๋„๋ฉ”์ธ ๊ฐ์ฒด๋“ค์„ ์ƒ์„ฑํ•˜๋Š” ํŒฉํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. + * + * ํ‰๊ฐ€๊ธฐ์™€ ๊ด€๋ จ๋œ ๊ฐ์ฒด๋“ค์„ ์ƒ์„ฑํ•˜๋ฉฐ, ๋„๋ฉ”์ธ ๊ทœ์น™๊ณผ ์ •์ฑ…์„ + * ์ ์šฉํ•˜์—ฌ ์ผ๊ด€๋œ ๊ฐ์ฒด ์ƒ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Factory(context = "evaluator", complexity = Complexity.NORMAL, cache = true) +class EvaluatorFactory( + private val configurationProvider: ConfigurationProvider +) { + + private val mathFunctionService = MathFunctionService(configurationProvider) + + /** + * ๋นˆ ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์œผ๋กœ ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createEvaluator(): ExpressionEvaluator { + return ExpressionEvaluator( + variables = emptyMap() + ) + } + + /** + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ๋งต์œผ๋กœ ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createEvaluator(variables: Map): ExpressionEvaluator { + return ExpressionEvaluator( + variables = variables.filterValues { it != null }.mapValues { it.value!! } + ) + } + + /** + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ๋ฆฌ์ŠคํŠธ๋กœ ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createEvaluator(bindings: List): ExpressionEvaluator { + val variableMap = bindings.associate { it.name to it.value } + return ExpressionEvaluator( + variables = variableMap.filterValues { it != null }.mapValues { it.value!! } + ) + } + + /** + * ์ˆ˜ํ•™ ์ƒ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createEvaluatorWithMathConstants(): ExpressionEvaluator { + val mathConstants = VariableBinding.getMathConstants() + return createEvaluator(mathConstants) + } + + /** + * ์ˆ˜ํ•™ ์ƒ์ˆ˜์™€ ์‚ฌ์šฉ์ž ๋ณ€์ˆ˜๊ฐ€ ํฌํ•จ๋œ ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createEvaluatorWithMathConstants(userVariables: Map): ExpressionEvaluator { + val mathConstants = VariableBinding.getMathConstants() + val allVariables = mutableMapOf() + + mathConstants.forEach { binding -> + allVariables[binding.name] = binding.value + } + + allVariables.putAll(userVariables) + + return createEvaluator(allVariables) + } + + /** + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createVariableBinding(name: String, value: Any?, isReadonly: Boolean = false): VariableBinding { + createdBindingCount++ + return VariableBinding.of(name, value, isReadonly) + } + + /** + * ์ˆซ์ž ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createNumberBinding(name: String, value: Double, isReadonly: Boolean = false): VariableBinding { + createdBindingCount++ + return VariableBinding.ofNumber(name, value, isReadonly) + } + + /** + * ๋ถˆ๋ฆฌ์–ธ ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createBooleanBinding(name: String, value: Boolean, isReadonly: Boolean = false): VariableBinding { + createdBindingCount++ + return VariableBinding.ofBoolean(name, value, isReadonly) + } + + /** + * ๋ฌธ์ž์—ด ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createStringBinding(name: String, value: String, isReadonly: Boolean = false): VariableBinding { + createdBindingCount++ + return VariableBinding.ofString(name, value, isReadonly) + } + + /** + * ์ฝ๊ธฐ ์ „์šฉ ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createReadonlyBinding(name: String, value: Any?): VariableBinding { + createdBindingCount++ + return VariableBinding.readonly(name, value) + } + + /** + * ์ƒ์ˆ˜ ๋ฐ”์ธ๋”ฉ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createConstantBinding(name: String, value: Any?): VariableBinding { + createdBindingCount++ + return VariableBinding.constant(name, value) + } + + /** + * ๊ฐ’ ๋งต์—์„œ ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createBindingsFromMap(valueMap: Map): List { + val bindings = VariableBinding.fromValueMap(valueMap) + createdBindingCount += bindings.size + return bindings + } + + /** + * ์ˆ˜ํ•™ ํ•จ์ˆ˜ ์„œ๋น„์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createMathFunctionService(): MathFunctionService { + createdMathServiceCount++ + return MathFunctionService(configurationProvider) + } + + /** + * ๊ธฐ๋ณธ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋“ค์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createDefaultEnvironment(): Map { + return EnvironmentFactory.createBasicEnvironment().mapValues { it.value } + } + + /** + * POC ์ฝ”๋“œ์˜ CalculatorService์™€ ์œ ์‚ฌํ•œ ๊ธฐ๋Šฅ์„ ๊ฐ€์ง„ ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createCalculatorServiceStyleEvaluator( + maxFormulaLength: Int = 5000, + enableOptimization: Boolean = true + ): ExpressionEvaluator { + val variables = mutableMapOf() + + // POC์˜ ๊ธฐ๋ณธ ์ˆ˜ํ•™ ์ƒ์ˆ˜๋“ค ์ถ”๊ฐ€ + variables.putAll(createDefaultEnvironment()) + + // ์ถ”๊ฐ€ ์ˆ˜ํ•™ ํ•จ์ˆ˜ ์ƒ์ˆ˜๋“ค + variables["ABS"] = "function" + variables["SQRT"] = "function" + variables["POW"] = "function" + variables["LOG"] = "function" + variables["LOG10"] = "function" + variables["EXP"] = "function" + variables["SIN"] = "function" + variables["COS"] = "function" + variables["TAN"] = "function" + + return ExpressionEvaluator( + variables = variables.filterValues { it != null }.mapValues { it.value!! } + ) + } + + /** + * POC ์ฝ”๋“œ์˜ RealLRParser์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ํ‰๊ฐ€๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createLRParserCompatibleEvaluator( + variables: Map = emptyMap() + ): ExpressionEvaluator { + val allVariables = mutableMapOf() + + // POC์˜ ๊ธฐ๋ณธ ๋ณ€์ˆ˜๋“ค + allVariables.putAll(createDefaultEnvironment()) + allVariables.putAll(variables) + + return ExpressionEvaluator( + variables = allVariables.filterValues { it != null }.mapValues { it.value!! } + ) + } + + /** + * ๊ณผํ•™ ๊ณ„์‚ฐ์šฉ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋“ค์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createScientificEnvironment(): Map { + return EnvironmentFactory.createScientificEnvironment().mapValues { it.value } + } + + /** + * ํ†ต๊ณ„ ๊ณ„์‚ฐ์šฉ ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋“ค์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createStatisticalEnvironment(): Map { + return EnvironmentFactory.createStatisticalEnvironment().mapValues { it.value } + } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createCustomEnvironment(customVariables: Map): Map { + return EnvironmentFactory.createCustomEnvironment( + customVariables.filterValues { it != null }.mapValues { it.value!! }, + EnvironmentFactory.createBasicEnvironment() + ).mapValues { it.value } + } + + /** + * ํŒฉํ† ๋ฆฌ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getFactoryStatistics(): Map { + return mapOf( + "totalEvaluatorsCreated" to createdEvaluatorCount, + "totalBindingsCreated" to createdBindingCount, + "mathFunctionServiceCreated" to createdMathServiceCount, + "factoryComplexity" to Complexity.NORMAL.name, + "cacheEnabled" to true + ) + } + + companion object { + private var createdEvaluatorCount = 0L + private var createdBindingCount = 0L + private var createdMathServiceCount = 0L + + @Volatile + private var instance: EvaluatorFactory? = null + + /** + * ์‹ฑ๊ธ€ํ†ค ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + @JvmStatic + fun getInstance(configurationProvider: ConfigurationProvider? = null): EvaluatorFactory { + return instance ?: synchronized(this) { + instance ?: EvaluatorFactory(configurationProvider ?: object : ConfigurationProvider { + override fun getParserConfiguration() = ParserConfiguration() + override fun getCalculatorConfiguration() = CalculatorConfiguration() + override fun getASTConfiguration() = ASTConfiguration() + override fun getLexerConfiguration() = LexerConfiguration() + override fun getExpresserConfiguration() = ExpresserConfiguration() + override fun getEvaluatorConfiguration() = EvaluatorConfiguration() + override fun updateParserConfiguration(configuration: ParserConfiguration) {} + override fun updateCalculatorConfiguration(configuration: CalculatorConfiguration) {} + override fun updateASTConfiguration(configuration: ASTConfiguration) {} + override fun updateLexerConfiguration(configuration: LexerConfiguration) {} + override fun updateExpresserConfiguration(configuration: ExpresserConfiguration) {} + override fun updateEvaluatorConfiguration(configuration: EvaluatorConfiguration) {} + override fun resetToDefaults() {} + override fun saveConfiguration() {} + override fun addConfigurationChangeListener(listener: ConfigurationChangeListener) {} + override fun removeConfigurationChangeListener(listener: ConfigurationChangeListener) {} + override fun validateConfiguration(): Map> = emptyMap() + override fun getConfigurationMetadata(): Map = emptyMap() + }).also { instance = it } + } + } + + /** + * ๋น ๋ฅธ ํ‰๊ฐ€๊ธฐ ์ƒ์„ฑ ํŽธ์˜ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + */ + @JvmStatic + fun quickCreateEvaluator(): ExpressionEvaluator { + return getInstance().createEvaluator() + } + + /** + * ์ˆ˜ํ•™ ์ƒ์ˆ˜ ํฌํ•จ ํ‰๊ฐ€๊ธฐ ์ƒ์„ฑ ํŽธ์˜ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + */ + @JvmStatic + fun quickCreateMathEvaluator(): ExpressionEvaluator { + return getInstance().createEvaluatorWithMathConstants() + } + + /** + * ๊ณผํ•™ ๊ณ„์‚ฐ์šฉ ํ‰๊ฐ€๊ธฐ ์ƒ์„ฑ ํŽธ์˜ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + */ + @JvmStatic + fun quickCreateScientificEvaluator(): ExpressionEvaluator { + val factory = getInstance() + return factory.createEvaluator(factory.createScientificEnvironment()) + } + } + + init { + createdEvaluatorCount++ + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/factories/MathFunctionFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/factories/MathFunctionFactory.kt new file mode 100644 index 00000000..d814f7fa --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/factories/MathFunctionFactory.kt @@ -0,0 +1,683 @@ +package hs.kr.entrydsm.domain.evaluator.factories + +import hs.kr.entrydsm.domain.evaluator.entities.MathFunction +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import kotlin.math.E +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.acos +import kotlin.math.acosh +import kotlin.math.asin +import kotlin.math.asinh +import kotlin.math.atan +import kotlin.math.atan2 +import kotlin.math.atanh +import kotlin.math.ceil +import kotlin.math.cos +import kotlin.math.cosh +import kotlin.math.exp +import kotlin.math.floor +import kotlin.math.ln +import kotlin.math.log10 +import kotlin.comparisons.minOf +import kotlin.math.pow +import kotlin.math.round +import kotlin.math.sign +import kotlin.math.sin +import kotlin.math.sinh +import kotlin.math.sqrt +import kotlin.math.tan +import kotlin.math.tanh +import kotlin.math.truncate + +/** + * ์ˆ˜ํ•™ ํ•จ์ˆ˜ ๊ฐ์ฒด๋“ค์„ ์ƒ์„ฑํ•˜๋Š” ํŒฉํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. + * + * DDD Factory ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค์˜ ์ƒ์„ฑ๊ณผ ๊ตฌ์„ฑ์„ + * ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋ฏธ๋ฆฌ ์ •์˜๋œ ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค์„ ์ƒ์„ฑํ•˜๊ณ  + * ์‚ฌ์šฉ์ž ์ •์˜ ํ•จ์ˆ˜์˜ ๋“ฑ๋ก์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Factory( + context = "evaluator", + complexity = Complexity.HIGH, + cache = true +) +class MathFunctionFactory { + + private val functionCache = mutableMapOf() + + /** + * ๊ธฐ๋ณธ ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค์˜ ๋งต + */ + fun createStandardFunctions(): Map { + return mapOf( + // ๊ธฐ๋ณธ ์‚ฐ์ˆ  ํ•จ์ˆ˜๋“ค + "ABS" to createAbsFunction(), + "SQRT" to createSqrtFunction(), + "ROUND" to createRoundFunction(), + "MIN" to createMinFunction(), + "MAX" to createMaxFunction(), + "SUM" to createSumFunction(), + "AVG" to createAvgFunction(), + + // ์ง€์ˆ˜ ๋ฐ ๋กœ๊ทธ ํ•จ์ˆ˜๋“ค + "POW" to createPowFunction(), + "LOG" to createLogFunction(), + "LOG10" to createLog10Function(), + "EXP" to createExpFunction(), + + // ์‚ผ๊ฐ ํ•จ์ˆ˜๋“ค + "SIN" to createSinFunction(), + "COS" to createCosFunction(), + "TAN" to createTanFunction(), + "ASIN" to createAsinFunction(), + "ACOS" to createAcosFunction(), + "ATAN" to createAtanFunction(), + "ATAN2" to createAtan2Function(), + + // ์Œ๊ณก ํ•จ์ˆ˜๋“ค + "SINH" to createSinhFunction(), + "COSH" to createCoshFunction(), + "TANH" to createTanhFunction(), + "ASINH" to createAsinhFunction(), + "ACOSH" to createAcoshFunction(), + "ATANH" to createAtanhFunction(), + + // ๋ฐ˜์˜ฌ๋ฆผ ๋ฐ ๋ฒ„๋ฆผ ํ•จ์ˆ˜๋“ค + "FLOOR" to createFloorFunction(), + "CEIL" to createCeilFunction(), + "TRUNC" to createTruncFunction(), + "SIGN" to createSignFunction(), + + // ์œ ํ‹ธ๋ฆฌํ‹ฐ ํ•จ์ˆ˜๋“ค + "IF" to createIfFunction(), + "RANDOM" to createRandomFunction(), + "RADIANS" to createRadiansFunction(), + "DEGREES" to createDegreesFunction(), + "PI" to createPiFunction(), + "E" to createEFunction(), + + // ๊ณ ๊ธ‰ ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค + "MOD" to createModFunction(), + "GCD" to createGcdFunction(), + "LCM" to createLcmFunction(), + "FACTORIAL" to createFactorialFunction(), + "COMBINATION" to createCombinationFunction(), + "PERMUTATION" to createPermutationFunction() + ) + } + + /** + * ์‚ผ๊ฐ ํ•จ์ˆ˜๋“ค๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ผ๊ฐ ํ•จ์ˆ˜๋“ค์˜ ๋งต + */ + fun createTrigonometricFunctions(): Map { + return mapOf( + "SIN" to createSinFunction(), + "COS" to createCosFunction(), + "TAN" to createTanFunction(), + "ASIN" to createAsinFunction(), + "ACOS" to createAcosFunction(), + "ATAN" to createAtanFunction(), + "ATAN2" to createAtan2Function() + ) + } + + /** + * ํ†ต๊ณ„ ํ•จ์ˆ˜๋“ค๋งŒ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ํ•จ์ˆ˜๋“ค์˜ ๋งต + */ + fun createStatisticalFunctions(): Map { + return mapOf( + "MIN" to createMinFunction(), + "MAX" to createMaxFunction(), + "SUM" to createSumFunction(), + "AVG" to createAvgFunction(), + "MEDIAN" to createMedianFunction(), + "MODE" to createModeFunction(), + "STDEV" to createStandardDeviationFunction(), + "VARIANCE" to createVarianceFunction() + ) + } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ํ•จ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜ ์ด๋ฆ„ + * @param minArgs ์ตœ์†Œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param maxArgs ์ตœ๋Œ€ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param description ํ•จ์ˆ˜ ์„ค๋ช… + * @param category ํ•จ์ˆ˜ ์นดํ…Œ๊ณ ๋ฆฌ + * @param implementation ํ•จ์ˆ˜ ๊ตฌํ˜„ + * @return ์ƒ์„ฑ๋œ MathFunction + */ + fun createCustomFunction( + name: String, + minArgs: Int, + maxArgs: Int, + description: String, + category: MathFunction.FunctionCategory, + implementation: (List) -> Any + ): MathFunction { + return MathFunction( + name = name.uppercase(), + minArguments = minArgs, + maxArguments = maxArgs, + description = description, + category = category, + implementation = implementation + ) + } + + // Standard Math Functions Implementation + + private fun createAbsFunction() = functionCache.getOrPut("ABS") { + MathFunction.fixedArgs( + "ABS", 1, "์ ˆ๋Œ“๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + abs(toDouble(args[0])) + } + } + + private fun createSqrtFunction() = functionCache.getOrPut("SQRT") { + MathFunction.fixedArgs( + "SQRT", 1, "์ œ๊ณฑ๊ทผ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + val value = toDouble(args[0]) + if (value < 0) throw EvaluatorException.mathError("์Œ์ˆ˜์˜ ์ œ๊ณฑ๊ทผ์€ ๊ณ„์‚ฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (์ž…๋ ฅ๊ฐ’: $value)") + sqrt(value) + } + } + + private fun createRoundFunction() = functionCache.getOrPut("ROUND") { + MathFunction.varArgs( + "ROUND", 1, 2, "๋ฐ˜์˜ฌ๋ฆผ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + when (args.size) { + 1 -> round(toDouble(args[0])) + 2 -> { + val value = toDouble(args[0]) + val places = toDouble(args[1]).toInt() + val multiplier = 10.0.pow(places.toDouble()) + round(value * multiplier) / multiplier + } + else -> throw EvaluatorException.wrongArgumentCount("ROUND", 2, args.size) + } + } + } + + private fun createMinFunction() = functionCache.getOrPut("MIN") { + MathFunction.varArgs( + "MIN", 1, Int.MAX_VALUE, "์ตœ์†Ÿ๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค", MathFunction.FunctionCategory.STATISTICAL + ) { args -> + args.map { toDouble(it) }.minOrNull() ?: throw EvaluatorException.wrongArgumentCount("MIN", 1, 0) + } + } + + private fun createMaxFunction() = functionCache.getOrPut("MAX") { + MathFunction.varArgs( + "MAX", 1, Int.MAX_VALUE, "์ตœ๋Œ“๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค", MathFunction.FunctionCategory.STATISTICAL + ) { args -> + args.map { toDouble(it) }.maxOrNull() ?: throw EvaluatorException.wrongArgumentCount("MAX", 1, 0) + } + } + + private fun createSumFunction() = functionCache.getOrPut("SUM") { + MathFunction.varArgs( + "SUM", 0, Int.MAX_VALUE, "ํ•ฉ๊ณ„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.STATISTICAL + ) { args -> + args.map { toDouble(it) }.sum() + } + } + + private fun createAvgFunction() = functionCache.getOrPut("AVG") { + MathFunction.varArgs( + "AVG", 1, Int.MAX_VALUE, "ํ‰๊ท ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.STATISTICAL + ) { args -> + args.map { toDouble(it) }.average() + } + } + + private fun createPowFunction() = functionCache.getOrPut("POW") { + MathFunction.fixedArgs( + "POW", 2, "๊ฑฐ๋“ญ์ œ๊ณฑ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + toDouble(args[0]).pow(toDouble(args[1])) + } + } + + private fun createLogFunction() = functionCache.getOrPut("LOG") { + MathFunction.fixedArgs( + "LOG", 1, "์ž์—ฐ๋กœ๊ทธ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.LOGARITHMIC + ) { args -> + val value = toDouble(args[0]) + if (value <= 0) throw EvaluatorException.mathError("๋กœ๊ทธ์˜ ์ธ์ˆ˜๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค (์ž…๋ ฅ๊ฐ’: $value)") + ln(value) + } + } + + private fun createLog10Function() = functionCache.getOrPut("LOG10") { + MathFunction.fixedArgs( + "LOG10", 1, "์ƒ์šฉ๋กœ๊ทธ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.LOGARITHMIC + ) { args -> + val value = toDouble(args[0]) + if (value <= 0) throw EvaluatorException.mathError("๋กœ๊ทธ์˜ ์ธ์ˆ˜๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค (์ž…๋ ฅ๊ฐ’: $value)") + log10(value) + } + } + + private fun createExpFunction() = functionCache.getOrPut("EXP") { + MathFunction.fixedArgs( + "EXP", 1, "์ง€์ˆ˜ํ•จ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.LOGARITHMIC + ) { args -> + exp(toDouble(args[0])) + } + } + + private fun createSinFunction() = functionCache.getOrPut("SIN") { + MathFunction.fixedArgs( + "SIN", 1, "์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.TRIGONOMETRIC + ) { args -> + sin(toDouble(args[0])) + } + } + + private fun createCosFunction() = functionCache.getOrPut("COS") { + MathFunction.fixedArgs( + "COS", 1, "์ฝ”์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.TRIGONOMETRIC + ) { args -> + cos(toDouble(args[0])) + } + } + + private fun createTanFunction() = functionCache.getOrPut("TAN") { + MathFunction.fixedArgs( + "TAN", 1, "ํƒ„์  ํŠธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.TRIGONOMETRIC + ) { args -> + tan(toDouble(args[0])) + } + } + + private fun createAsinFunction() = functionCache.getOrPut("ASIN") { + MathFunction.fixedArgs( + "ASIN", 1, "์•„ํฌ์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.TRIGONOMETRIC + ) { args -> + val value = toDouble(args[0]) + if (value < -1 || value > 1) throw EvaluatorException.mathError("ASIN ํ•จ์ˆ˜์˜ ์ •์˜์—ญ ์˜ค๋ฅ˜: ์ž…๋ ฅ๊ฐ’์€ [-1, 1] ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค (์ž…๋ ฅ๊ฐ’: $value)") + asin(value) + } + } + + private fun createAcosFunction() = functionCache.getOrPut("ACOS") { + MathFunction.fixedArgs( + "ACOS", 1, "์•„ํฌ์ฝ”์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.TRIGONOMETRIC + ) { args -> + val value = toDouble(args[0]) + if (value < -1 || value > 1) throw EvaluatorException.mathError("ACOS ํ•จ์ˆ˜์˜ ์ •์˜์—ญ ์˜ค๋ฅ˜: ์ž…๋ ฅ๊ฐ’์€ [-1, 1] ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค (์ž…๋ ฅ๊ฐ’: $value)") + acos(value) + } + } + + private fun createAtanFunction() = functionCache.getOrPut("ATAN") { + MathFunction.fixedArgs( + "ATAN", 1, "์•„ํฌํƒ„์  ํŠธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.TRIGONOMETRIC + ) { args -> + atan(toDouble(args[0])) + } + } + + private fun createAtan2Function() = functionCache.getOrPut("ATAN2") { + MathFunction.fixedArgs( + "ATAN2", 2, "2๊ฐœ ์ธ์ˆ˜์˜ ์•„ํฌํƒ„์  ํŠธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.TRIGONOMETRIC + ) { args -> + atan2(toDouble(args[0]), toDouble(args[1])) + } + } + + private fun createSinhFunction() = functionCache.getOrPut("SINH") { + MathFunction.fixedArgs( + "SINH", 1, "ํ•˜์ดํผ๋ณผ๋ฆญ ์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.HYPERBOLIC + ) { args -> + sinh(toDouble(args[0])) + } + } + + private fun createCoshFunction() = functionCache.getOrPut("COSH") { + MathFunction.fixedArgs( + "COSH", 1, "ํ•˜์ดํผ๋ณผ๋ฆญ ์ฝ”์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.HYPERBOLIC + ) { args -> + cosh(toDouble(args[0])) + } + } + + private fun createTanhFunction() = functionCache.getOrPut("TANH") { + MathFunction.fixedArgs( + "TANH", 1, "ํ•˜์ดํผ๋ณผ๋ฆญ ํƒ„์  ํŠธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.HYPERBOLIC + ) { args -> + tanh(toDouble(args[0])) + } + } + + private fun createAsinhFunction() = functionCache.getOrPut("ASINH") { + MathFunction.fixedArgs( + "ASINH", 1, "์—ญ ํ•˜์ดํผ๋ณผ๋ฆญ ์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.HYPERBOLIC + ) { args -> + asinh(toDouble(args[0])) + } + } + + private fun createAcoshFunction() = functionCache.getOrPut("ACOSH") { + MathFunction.fixedArgs( + "ACOSH", 1, "์—ญ ํ•˜์ดํผ๋ณผ๋ฆญ ์ฝ”์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.HYPERBOLIC + ) { args -> + val value = toDouble(args[0]) + if (value < 1) throw EvaluatorException.mathError("ACOSH ํ•จ์ˆ˜์˜ ์ •์˜์—ญ ์˜ค๋ฅ˜: ์ž…๋ ฅ๊ฐ’์€ 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค (์ž…๋ ฅ๊ฐ’: $value)") + acosh(value) + } + } + + private fun createAtanhFunction() = functionCache.getOrPut("ATANH") { + MathFunction.fixedArgs( + "ATANH", 1, "์—ญ ํ•˜์ดํผ๋ณผ๋ฆญ ํƒ„์  ํŠธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.HYPERBOLIC + ) { args -> + val value = toDouble(args[0]) + if (value <= -1 || value >= 1) throw EvaluatorException.mathError("ATANH ํ•จ์ˆ˜์˜ ์ •์˜์—ญ ์˜ค๋ฅ˜: ์ž…๋ ฅ๊ฐ’์€ (-1, 1) ๋ฒ”์œ„์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค (์ž…๋ ฅ๊ฐ’: $value)") + atanh(value) + } + } + + private fun createFloorFunction() = functionCache.getOrPut("FLOOR") { + MathFunction.fixedArgs( + "FLOOR", 1, "๋‚ด๋ฆผ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + floor(toDouble(args[0])) + } + } + + private fun createCeilFunction() = functionCache.getOrPut("CEIL") { + MathFunction.fixedArgs( + "CEIL", 1, "์˜ฌ๋ฆผ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + ceil(toDouble(args[0])) + } + } + + private fun createTruncFunction() = functionCache.getOrPut("TRUNC") { + MathFunction.fixedArgs( + "TRUNC", 1, "๋ฒ„๋ฆผ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + truncate(toDouble(args[0])) + } + } + + private fun createSignFunction() = functionCache.getOrPut("SIGN") { + MathFunction.fixedArgs( + "SIGN", 1, "๋ถ€ํ˜ธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + sign(toDouble(args[0])) + } + } + + private fun createIfFunction() = functionCache.getOrPut("IF") { + MathFunction.fixedArgs( + "IF", 3, "์กฐ๊ฑด๋ฌธ์„ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.LOGICAL + ) { args -> + val condition = toBoolean(args[0]) + if (condition) args[1] else args[2] + } + } + + private fun createRandomFunction() = functionCache.getOrPut("RANDOM") { + MathFunction.fixedArgs( + "RANDOM", 0, "๋‚œ์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.UTILITY + ) { _ -> + kotlin.random.Random.nextDouble() + } + } + + private fun createRadiansFunction() = functionCache.getOrPut("RADIANS") { + MathFunction.fixedArgs( + "RADIANS", 1, "๋„๋ฅผ ๋ผ๋””์•ˆ์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.CONVERSION + ) { args -> + toDouble(args[0]) * PI / 180.0 + } + } + + private fun createDegreesFunction() = functionCache.getOrPut("DEGREES") { + MathFunction.fixedArgs( + "DEGREES", 1, "๋ผ๋””์•ˆ์„ ๋„๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.CONVERSION + ) { args -> + toDouble(args[0]) * 180.0 / PI + } + } + + private fun createPiFunction() = functionCache.getOrPut("PI") { + MathFunction.fixedArgs( + "PI", 0, "์›์ฃผ์œจ ฯ€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.UTILITY + ) { _ -> + PI + } + } + + private fun createEFunction() = functionCache.getOrPut("E") { + MathFunction.fixedArgs( + "E", 0, "์ž์—ฐ์ƒ์ˆ˜ e๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.UTILITY + ) { _ -> + E + } + } + + private fun createModFunction() = functionCache.getOrPut("MOD") { + MathFunction.fixedArgs( + "MOD", 2, "๋‚˜๋จธ์ง€๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + val dividend = toDouble(args[0]) + val divisor = toDouble(args[1]) + if (divisor == 0.0) throw EvaluatorException.divisionByZero("MOD") + dividend % divisor + } + } + + private fun createGcdFunction() = functionCache.getOrPut("GCD") { + MathFunction.fixedArgs( + "GCD", 2, "์ตœ๋Œ€๊ณต์•ฝ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + val a = toDouble(args[0]).toLong() + val b = toDouble(args[1]).toLong() + gcd(a, b).toDouble() + } + } + + private fun createLcmFunction() = functionCache.getOrPut("LCM") { + MathFunction.fixedArgs( + "LCM", 2, "์ตœ์†Œ๊ณต๋ฐฐ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + val a = toDouble(args[0]).toLong() + val b = toDouble(args[1]).toLong() + lcm(a, b).toDouble() + } + } + + private fun createFactorialFunction() = functionCache.getOrPut("FACTORIAL") { + MathFunction.fixedArgs( + "FACTORIAL", 1, "ํŒฉํ† ๋ฆฌ์–ผ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + val n = toDouble(args[0]).toInt() + factorial(n).toDouble() + } + } + + private fun createCombinationFunction() = functionCache.getOrPut("COMBINATION") { + MathFunction.fixedArgs( + "COMBINATION", 2, "์กฐํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + val n = toDouble(args[0]).toInt() + val r = toDouble(args[1]).toInt() + if (n < 0 || r < 0 || r > n) throw EvaluatorException.mathError("์กฐํ•ฉ ํ•จ์ˆ˜์˜ ์ •์˜์—ญ ์˜ค๋ฅ˜: n >= 0, r >= 0, r <= n ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค (n: $n, r: $r)") + combination(n, r).toDouble() + } + } + + private fun createPermutationFunction() = functionCache.getOrPut("PERMUTATION") { + MathFunction.fixedArgs( + "PERMUTATION", 2, "์ˆœ์—ด์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.ARITHMETIC + ) { args -> + val n = toDouble(args[0]).toInt() + val r = toDouble(args[1]).toInt() + if (n < 0 || r < 0 || r > n) throw EvaluatorException.mathError("์ˆœ์—ด ํ•จ์ˆ˜์˜ ์ •์˜์—ญ ์˜ค๋ฅ˜: n >= 0, r >= 0, r <= n ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค (n: $n, r: $r)") + permutation(n, r).toDouble() + } + } + + // Statistical Functions + + private fun createMedianFunction() = functionCache.getOrPut("MEDIAN") { + MathFunction.varArgs( + "MEDIAN", 1, Int.MAX_VALUE, "์ค‘์•™๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.STATISTICAL + ) { args -> + val values = args.map { toDouble(it) }.sorted() + val size = values.size + if (size % 2 == 0) { + (values[size / 2 - 1] + values[size / 2]) / 2.0 + } else { + values[size / 2] + } + } + } + + private fun createModeFunction() = functionCache.getOrPut("MODE") { + MathFunction.varArgs( + "MODE", 1, Int.MAX_VALUE, "์ตœ๋นˆ๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค", MathFunction.FunctionCategory.STATISTICAL + ) { args -> + val values = args.map { toDouble(it) } + val frequency = values.groupBy { it }.mapValues { it.value.size } + val maxFreq = frequency.maxByOrNull { it.value }?.value ?: 0 + frequency.filter { it.value == maxFreq }.keys.minOrNull() ?: 0.0 + } + } + + private fun createStandardDeviationFunction() = functionCache.getOrPut("STDEV") { + MathFunction.varArgs( + "STDEV", 1, Int.MAX_VALUE, "ํ‘œ์ค€ํŽธ์ฐจ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.STATISTICAL + ) { args -> + val values = args.map { toDouble(it) } + val mean = values.average() + val variance = values.map { (it - mean).pow(2) }.average() + sqrt(variance) + } + } + + private fun createVarianceFunction() = functionCache.getOrPut("VARIANCE") { + MathFunction.varArgs( + "VARIANCE", 1, Int.MAX_VALUE, "๋ถ„์‚ฐ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค", MathFunction.FunctionCategory.STATISTICAL + ) { args -> + val values = args.map { toDouble(it) } + val mean = values.average() + values.map { (it - mean).pow(2) }.average() + } + } + + // Helper Functions + + private fun toDouble(value: Any): Double { + return when (value) { + is Double -> value + is Int -> value.toDouble() + is Float -> value.toDouble() + is Long -> value.toDouble() + is String -> value.toDoubleOrNull() ?: throw EvaluatorException.numberConversionError(value) + else -> throw EvaluatorException.unsupportedType(value::class.simpleName ?: "Unknown", value) + } + } + + private fun toBoolean(value: Any): Boolean { + return when (value) { + is Boolean -> value + is Double -> value != 0.0 && !value.isNaN() + is Int -> value != 0 + is String -> value.isNotEmpty() && value.lowercase() !in setOf("false", "0") + else -> true + } + } + + private fun gcd(a: Long, b: Long): Long { + return if (b == 0L) abs(a) else gcd(b, a % b) + } + + private fun lcm(a: Long, b: Long): Long { + return abs(a * b) / gcd(a, b) + } + + private fun factorial(n: Int): Long { + if (n < 0) throw EvaluatorException.mathError("์Œ์ˆ˜์˜ ํŒฉํ† ๋ฆฌ์–ผ์€ ๊ณ„์‚ฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (์ž…๋ ฅ๊ฐ’: $n)") + if (n > 20) throw EvaluatorException.mathError("ํŒฉํ† ๋ฆฌ์–ผ ๊ณ„์‚ฐ ๋ฒ”์œ„ ์ดˆ๊ณผ: n์€ 20 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค (Long ํƒ€์ž… ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๋ฐฉ์ง€, ์ž…๋ ฅ๊ฐ’: $n)") + if (n <= 1) return 1 + var result = 1L + for (i in 2..n) { + result *= i + } + return result + } + + private fun combination(n: Int, r: Int): Long { + if (r > n || r < 0) return 0 + if (r == 0 || r == n) return 1 + + val k = minOf(r, n - r) + var result = 1L + + for (i in 0 until k) { + result = result * (n - i) / (i + 1) + } + + return result + } + + private fun permutation(n: Int, r: Int): Long { + if (r > n || r < 0) return 0 + if (r == 0) return 1 + + var result = 1L + for (i in 0 until r) { + result *= (n - i) + } + + return result + } + + /** + * ํŒฉํ† ๋ฆฌ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "factoryName" to "MathFunctionFactory", + "standardFunctionCount" to createStandardFunctions().size, + "trigonometricFunctionCount" to createTrigonometricFunctions().size, + "statisticalFunctionCount" to createStatisticalFunctions().size, + "cacheEnabled" to true, + "complexityLevel" to Complexity.HIGH.name + ) + + /** + * ํŒฉํ† ๋ฆฌ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "cachedFunctions" to functionCache.size, + "supportedCategories" to MathFunction.FunctionCategory.values().size, + "totalStandardFunctions" to createStandardFunctions().size + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/functions/MathFunctions.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/functions/MathFunctions.kt new file mode 100644 index 00000000..0031679d --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/functions/MathFunctions.kt @@ -0,0 +1,236 @@ +package hs.kr.entrydsm.domain.evaluator.functions + +import hs.kr.entrydsm.domain.evaluator.interfaces.FunctionEvaluator +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import kotlin.math.* + +/** + * ๊ธฐ๋ณธ ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค์˜ ๊ตฌํ˜„์ฒด์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.06 + */ + +class AbsFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 1) + return abs(toDouble(args[0])) + } + + override fun getSupportedArgumentCounts(): List = listOf(1) + override fun getFunctionName(): String = "ABS" + override fun getDescription(): String = "์ ˆ๋Œ€๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +class SqrtFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 1) + val value = toDouble(args[0]) + if (value < 0) throw EvaluatorException.mathError("SQRT of negative number") + return sqrt(value) + } + + override fun getSupportedArgumentCounts(): List = listOf(1) + override fun getFunctionName(): String = "SQRT" + override fun getDescription(): String = "์ œ๊ณฑ๊ทผ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +class RoundFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + return when (args.size) { + 1 -> round(toDouble(args[0])) + 2 -> { + val value = toDouble(args[0]) + val places = toDouble(args[1]).toInt() + val multiplier = 10.0.pow(places.toDouble()) + round(value * multiplier) / multiplier + } + else -> throw EvaluatorException.wrongArgumentCount("ROUND", 1, args.size) + } + } + + override fun getSupportedArgumentCounts(): List = listOf(1, 2) + override fun getFunctionName(): String = "ROUND" + override fun getDescription(): String = "๋ฐ˜์˜ฌ๋ฆผํ•ฉ๋‹ˆ๋‹ค" +} + +class MinFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + if (args.isEmpty()) throw EvaluatorException.wrongArgumentCount("MIN", 1, 0) + return args.map { toDouble(it) }.minOrNull() ?: 0.0 + } + + override fun getSupportedArgumentCounts(): List? = null // ๊ฐ€๋ณ€ ์ธ์ˆ˜ + override fun getFunctionName(): String = "MIN" + override fun getDescription(): String = "์ตœ์†Ÿ๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค" +} + +class MaxFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + if (args.isEmpty()) throw EvaluatorException.wrongArgumentCount("MAX", 1, 0) + return args.map { toDouble(it) }.maxOrNull() ?: 0.0 + } + + override fun getSupportedArgumentCounts(): List? = null // ๊ฐ€๋ณ€ ์ธ์ˆ˜ + override fun getFunctionName(): String = "MAX" + override fun getDescription(): String = "์ตœ๋Œ“๊ฐ’์„ ์ฐพ์Šต๋‹ˆ๋‹ค" +} + +class SumFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + return args.map { toDouble(it) }.sum() + } + + override fun getSupportedArgumentCounts(): List? = null // ๊ฐ€๋ณ€ ์ธ์ˆ˜ + override fun getFunctionName(): String = "SUM" + override fun getDescription(): String = "ํ•ฉ๊ณ„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +class AvgFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + if (args.isEmpty()) throw EvaluatorException.wrongArgumentCount("AVG", 1, 0) + return args.map { toDouble(it) }.average() + } + + override fun getSupportedArgumentCounts(): List? = null // ๊ฐ€๋ณ€ ์ธ์ˆ˜ + override fun getFunctionName(): String = "AVG" + override fun getDescription(): String = "ํ‰๊ท ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +class AverageFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + if (args.isEmpty()) throw EvaluatorException.wrongArgumentCount("AVERAGE", 1, 0) + return args.map { toDouble(it) }.average() + } + + override fun getSupportedArgumentCounts(): List? = null // ๊ฐ€๋ณ€ ์ธ์ˆ˜ + override fun getFunctionName(): String = "AVERAGE" + override fun getDescription(): String = "ํ‰๊ท ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +class IfFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 3) + val condition = toBoolean(args[0]) + return if (condition) args[1] else args[2] + } + + override fun getSupportedArgumentCounts(): List = listOf(3) + override fun getFunctionName(): String = "IF" + override fun getDescription(): String = "์กฐ๊ฑด๋ถ€ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค" +} + +class PowFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 2) + return toDouble(args[0]).pow(toDouble(args[1])) + } + + override fun getSupportedArgumentCounts(): List = listOf(2) + override fun getFunctionName(): String = "POW" + override fun getDescription(): String = "๊ฑฐ๋“ญ์ œ๊ณฑ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +// ์‚ผ๊ฐํ•จ์ˆ˜๋“ค +class SinFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 1) + return sin(toDouble(args[0])) + } + + override fun getSupportedArgumentCounts(): List = listOf(1) + override fun getFunctionName(): String = "SIN" + override fun getDescription(): String = "์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +class CosFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 1) + return cos(toDouble(args[0])) + } + + override fun getSupportedArgumentCounts(): List = listOf(1) + override fun getFunctionName(): String = "COS" + override fun getDescription(): String = "์ฝ”์‚ฌ์ธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +class TanFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 1) + return tan(toDouble(args[0])) + } + + override fun getSupportedArgumentCounts(): List = listOf(1) + override fun getFunctionName(): String = "TAN" + override fun getDescription(): String = "ํƒ„์  ํŠธ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +// ๋กœ๊ทธ ํ•จ์ˆ˜๋“ค +class LogFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 1) + val value = toDouble(args[0]) + if (value <= 0) throw EvaluatorException.mathError("LOG of non-positive number") + return ln(value) + } + + override fun getSupportedArgumentCounts(): List = listOf(1) + override fun getFunctionName(): String = "LOG" + override fun getDescription(): String = "์ž์—ฐ๋กœ๊ทธ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +class Log10Function : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 1) + val value = toDouble(args[0]) + if (value <= 0) throw EvaluatorException.mathError("LOG10 of non-positive number") + return log10(value) + } + + override fun getSupportedArgumentCounts(): List = listOf(1) + override fun getFunctionName(): String = "LOG10" + override fun getDescription(): String = "์ƒ์šฉ๋กœ๊ทธ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +class ExpFunction : FunctionEvaluator { + override fun evaluate(args: List): Any? { + validateArgumentCount(args, 1) + return exp(toDouble(args[0])) + } + + override fun getSupportedArgumentCounts(): List = listOf(1) + override fun getFunctionName(): String = "EXP" + override fun getDescription(): String = "์ง€์ˆ˜ํ•จ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค" +} + +// Helper functions +private fun validateArgumentCount(args: List, expectedCount: Int) { + if (args.size != expectedCount) { + throw EvaluatorException.wrongArgumentCount("function", expectedCount, args.size) + } +} + +private fun toDouble(value: Any?): Double { + return when (value) { + is Double -> value + is Int -> value.toDouble() + is Float -> value.toDouble() + is Long -> value.toDouble() + is String -> value.toDoubleOrNull() + ?: throw EvaluatorException.numberConversionError(value) + else -> throw EvaluatorException.numberConversionError(value) + } +} + +private fun toBoolean(value: Any?): Boolean { + return when (value) { + is Boolean -> value + is Double -> value != 0.0 + is Int -> value != 0 + is Float -> value != 0.0f + is Long -> value != 0L + is String -> value.isNotEmpty() + null -> false + else -> true + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/interfaces/ASTVisitorContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/interfaces/ASTVisitorContract.kt new file mode 100644 index 00000000..92fd9205 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/interfaces/ASTVisitorContract.kt @@ -0,0 +1,117 @@ +package hs.kr.entrydsm.domain.evaluator.interfaces + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.ArgumentsNode +import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode +import hs.kr.entrydsm.domain.ast.entities.BooleanNode +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.entities.IfNode +import hs.kr.entrydsm.domain.ast.entities.NumberNode +import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode +import hs.kr.entrydsm.domain.ast.entities.VariableNode +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException + +/** + * AST ๋ฐฉ๋ฌธ์ž ํŒจํ„ด์˜ ๊ณ„์•ฝ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Visitor ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ AST ๋…ธ๋“œ์˜ ๋‹ค์–‘ํ•œ ์ฒ˜๋ฆฌ ๋กœ์ง์„ + * ๋ถ„๋ฆฌํ•˜๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๊ตฌ์กฐ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ AST ๋…ธ๋“œ ํƒ€์ž…์— ๋Œ€ํ•œ + * ๋ฐฉ๋ฌธ ๋ฉ”์„œ๋“œ๋ฅผ ์ •์˜ํ•˜์—ฌ ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface ASTVisitorContract { + + /** + * NumberNode๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  NumberNode + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitNumber(node: NumberNode): Any? + + /** + * BooleanNode๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  BooleanNode + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitBoolean(node: BooleanNode): Any? + + /** + * VariableNode๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  VariableNode + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitVariable(node: VariableNode): Any? + + /** + * BinaryOpNode๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  BinaryOpNode + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitBinaryOp(node: BinaryOpNode): Any? + + /** + * UnaryOpNode๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  UnaryOpNode + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitUnaryOp(node: UnaryOpNode): Any? + + /** + * FunctionCallNode๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  FunctionCallNode + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitFunctionCall(node: FunctionCallNode): Any? + + /** + * IfNode๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  IfNode + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitIf(node: IfNode): Any? + + /** + * ArgumentsNode๋ฅผ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ArgumentsNode + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visitArguments(node: ArgumentsNode): Any? + + /** + * AST ๋…ธ๋“œ ๋ฐฉ๋ฌธ์„ ์œ„ํ•œ ๊ธฐ๋ณธ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ASTVisitor ๊ตฌํ˜„์ฒด์—์„œ๋งŒ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  AST ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + * @throws EvaluatorException ์บ์ŠคํŒ… ์‹คํŒจ ์‹œ ์˜ˆ์™ธ ๋ฐœ์ƒ + */ + fun visit(node: ASTNode): T { + return try { + val visitor = this as? ASTVisitor + ?: throw EvaluatorException.unsupportedType( + valueType = this::class.simpleName ?: "Unknown", + value = "ASTVisitorContract ๊ตฌํ˜„์ฒด๊ฐ€ ASTVisitor๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + ) + node.accept(visitor) + } catch (e: ClassCastException) { + throw EvaluatorException.unsupportedType( + valueType = this::class.simpleName ?: "Unknown", + value = "ASTVisitor๋กœ ์บ์ŠคํŒ…ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/interfaces/EvaluatorContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/interfaces/EvaluatorContract.kt new file mode 100644 index 00000000..6817cf15 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/interfaces/EvaluatorContract.kt @@ -0,0 +1,144 @@ +package hs.kr.entrydsm.domain.evaluator.interfaces + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.evaluator.values.EvaluationResult +import hs.kr.entrydsm.domain.evaluator.values.VariableBinding + +/** + * ํ‘œํ˜„์‹ ํ‰๊ฐ€์ž์˜ ๊ณ„์•ฝ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Anti-Corruption Layer ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ํ‰๊ฐ€ ๊ตฌํ˜„์ฒด๋“ค ๊ฐ„์˜ + * ํ˜ธํ™˜์„ฑ์„ ๋ณด์žฅํ•˜๋ฉฐ, ํ‘œํ˜„์‹ ํ‰๊ฐ€์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ํ‘œ์ค€ํ™”๋œ ๋ฐฉ์‹์œผ๋กœ + * ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. DDD ์ธํ„ฐํŽ˜์ด์Šค ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๊ตฌํ˜„์ฒด์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์˜ + * ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถฅ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface EvaluatorContract { + + /** + * AST ๋…ธ๋“œ๋ฅผ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ํ‰๊ฐ€ํ•  AST ๋…ธ๋“œ + * @return ํ‰๊ฐ€ ๊ฒฐ๊ณผ + */ + fun evaluate(node: ASTNode): EvaluationResult + + /** + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ๊ณผ ํ•จ๊ป˜ AST ๋…ธ๋“œ๋ฅผ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ํ‰๊ฐ€ํ•  AST ๋…ธ๋“œ + * @param variables ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ + * @return ํ‰๊ฐ€ ๊ฒฐ๊ณผ + */ + fun evaluate(node: ASTNode, variables: VariableBinding): EvaluationResult + + /** + * ๋ณ€์ˆ˜ ๋งต๊ณผ ํ•จ๊ป˜ AST ๋…ธ๋“œ๋ฅผ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ํ‰๊ฐ€ํ•  AST ๋…ธ๋“œ + * @param variables ๋ณ€์ˆ˜ ๋งต + * @return ํ‰๊ฐ€ ๊ฒฐ๊ณผ + */ + fun evaluate(node: ASTNode, variables: Map): EvaluationResult + + /** + * ํ‘œํ˜„์‹์ด ํ‰๊ฐ€ ๊ฐ€๋Šฅํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ํ‰๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun canEvaluate(node: ASTNode): Boolean + + /** + * ํ‘œํ˜„์‹์ด ํ‰๊ฐ€ ๊ฐ€๋Šฅํ•œ์ง€ ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ๊ณผ ํ•จ๊ป˜ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @param variables ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ + * @return ํ‰๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun canEvaluate(node: ASTNode, variables: VariableBinding): Boolean + + /** + * ํ‘œํ˜„์‹์—์„œ ์‚ฌ์šฉ๋œ ๋ณ€์ˆ˜ ๋ชฉ๋ก์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @return ์‚ฌ์šฉ๋œ ๋ณ€์ˆ˜ ์ง‘ํ•ฉ + */ + fun extractVariables(node: ASTNode): Set + + /** + * ํ‘œํ˜„์‹์—์„œ ์‚ฌ์šฉ๋œ ํ•จ์ˆ˜ ๋ชฉ๋ก์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @return ์‚ฌ์šฉ๋œ ํ•จ์ˆ˜ ์ง‘ํ•ฉ + */ + fun extractFunctions(node: ASTNode): Set + + /** + * ํ‘œํ˜„์‹์˜ ๋ณต์žก๋„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @return ๋ณต์žก๋„ ์ˆ˜์น˜ + */ + fun calculateComplexity(node: ASTNode): Int + + /** + * ํ‘œํ˜„์‹์˜ ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @return ํŠธ๋ฆฌ ๊นŠ์ด + */ + fun calculateDepth(node: ASTNode): Int + + /** + * ํ‘œํ˜„์‹์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ์ตœ์ ํ™”ํ•  AST ๋…ธ๋“œ + * @return ์ตœ์ ํ™”๋œ AST ๋…ธ๋“œ + */ + fun optimize(node: ASTNode): ASTNode + + /** + * ์ง€์›๋˜๋Š” ํ•จ์ˆ˜ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ํ•จ์ˆ˜ ์ด๋ฆ„ ์ง‘ํ•ฉ + */ + fun getSupportedFunctions(): Set + + /** + * ์ง€์›๋˜๋Š” ์—ฐ์‚ฐ์ž ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ์—ฐ์‚ฐ์ž ์ง‘ํ•ฉ + */ + fun getSupportedOperators(): Set + + /** + * ํ‰๊ฐ€์ž์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map + + /** + * ํ‰๊ฐ€์ž์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map + + /** + * ํ‰๊ฐ€์ž๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun reset() + + /** + * ํ‰๊ฐ€์ž๊ฐ€ ํ™œ์„ฑ ์ƒํƒœ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ™œ์„ฑ ์ƒํƒœ์ด๋ฉด true + */ + fun isActive(): Boolean +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/interfaces/FunctionEvaluator.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/interfaces/FunctionEvaluator.kt new file mode 100644 index 00000000..e87cacb5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/interfaces/FunctionEvaluator.kt @@ -0,0 +1,44 @@ +package hs.kr.entrydsm.domain.evaluator.interfaces + +/** + * ํ•จ์ˆ˜ ํ‰๊ฐ€๋ฅผ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ๊ฐ ํ•จ์ˆ˜๋ณ„๋กœ ๋ณ„๋„์˜ ํด๋ž˜์Šค๋ฅผ ๋งŒ๋“ค์–ด ๋ชจ๋“ˆํ™”ํ•˜๊ณ , + * ํ•จ์ˆ˜ ์ถ”๊ฐ€/์ˆ˜์ •/ํ…Œ์ŠคํŠธ๋ฅผ ์šฉ์ดํ•˜๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.06 + */ +interface FunctionEvaluator { + + /** + * ํ•จ์ˆ˜๋ฅผ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param args ํ•จ์ˆ˜ ์ธ์ˆ˜ ๋ชฉ๋ก + * @return ํ‰๊ฐ€ ๊ฒฐ๊ณผ + * @throws hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException ์ž˜๋ชป๋œ ์ธ์ˆ˜๊ฐ€ ์ „๋‹ฌ๋˜๊ฑฐ๋‚˜ ์ˆ˜ํ•™์  ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๊ฒฝ์šฐ + */ + fun evaluate(args: List): Any? + + /** + * ํ•จ์ˆ˜๊ฐ€ ์ง€์›ํ•˜๋Š” ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * null์ธ ๊ฒฝ์šฐ ๊ฐ€๋ณ€ ์ธ์ˆ˜๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›ํ•˜๋Š” ์ธ์ˆ˜ ๊ฐœ์ˆ˜ ๋˜๋Š” null (๊ฐ€๋ณ€ ์ธ์ˆ˜) + */ + fun getSupportedArgumentCounts(): List? + + /** + * ํ•จ์ˆ˜๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•จ์ˆ˜๋ช… + */ + fun getFunctionName(): String + + /** + * ํ•จ์ˆ˜ ์„ค๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•จ์ˆ˜ ์„ค๋ช… + */ + fun getDescription(): String +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/poc-code.md b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/poc-code.md similarity index 99% rename from casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/poc-code.md rename to casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/poc-code.md index e8205a81..c430da19 100644 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/calculator/poc-code.md +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/poc-code.md @@ -1,4 +1,4 @@ -```kt +"```kt package com.example.calculator import org.slf4j.LoggerFactory @@ -408,21 +408,21 @@ object OperatorPrecedenceTable { TokenType.LESS_EQUAL to OperatorPrecedence(4, Associativity.NONE), TokenType.GREATER to OperatorPrecedence(4, Associativity.NONE), TokenType.GREATER_EQUAL to OperatorPrecedence(4, Associativity.NONE), - + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž TokenType.PLUS to OperatorPrecedence(5, Associativity.LEFT), TokenType.MINUS to OperatorPrecedence(5, Associativity.LEFT), TokenType.MULTIPLY to OperatorPrecedence(6, Associativity.LEFT), TokenType.DIVIDE to OperatorPrecedence(6, Associativity.LEFT), TokenType.MODULO to OperatorPrecedence(6, Associativity.LEFT), - + // ๊ฑฐ๋“ญ์ œ๊ณฑ (๊ฐ€์žฅ ๋†’์€ ์šฐ์„ ์ˆœ์œ„) TokenType.POWER to OperatorPrecedence(7, Associativity.RIGHT), - + // ๋‹จํ•ญ ์—ฐ์‚ฐ์ž (NOT, unary +, unary -)๋Š” ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ํŠน๋ณ„ ์ฒ˜๋ฆฌ TokenType.NOT to OperatorPrecedence(8, Associativity.RIGHT) ) - + fun getPrecedence(token: TokenType): OperatorPrecedence? = precedenceMap[token] } @@ -585,59 +585,59 @@ data class Production( sealed class ASTBuilder { abstract fun build(children: List): Any - + object Identity : ASTBuilder() { override fun build(children: List) = children[0] as ASTNode } - + object Start : ASTBuilder() { override fun build(children: List) = children[0] as ASTNode } - + class BinaryOp(private val operator: String, private val leftIndex: Int = 0, private val rightIndex: Int = 2) : ASTBuilder() { override fun build(children: List) = BinaryOpNode(children[leftIndex] as ASTNode, operator, children[rightIndex] as ASTNode) } - + class UnaryOp(private val operator: String, private val operandIndex: Int = 1) : ASTBuilder() { override fun build(children: List) = UnaryOpNode(operator, children[operandIndex] as ASTNode) } - + object Number : ASTBuilder() { override fun build(children: List) = NumberNode((children[0] as Token).value.toDouble()) } - + object Variable : ASTBuilder() { override fun build(children: List) = VariableNode((children[0] as Token).value) } - + object BooleanTrue : ASTBuilder() { override fun build(children: List) = BooleanNode(true) } - + object BooleanFalse : ASTBuilder() { override fun build(children: List) = BooleanNode(false) } - + object Parenthesized : ASTBuilder() { override fun build(children: List) = children[1] as ASTNode } - + object FunctionCall : ASTBuilder() { override fun build(children: List) = FunctionCallNode((children[0] as Token).value, children[2] as List) } - + object FunctionCallEmpty : ASTBuilder() { override fun build(children: List) = FunctionCallNode((children[0] as Token).value, emptyList()) } - + object If : ASTBuilder() { override fun build(children: List) = IfNode(children[2] as ASTNode, children[4] as ASTNode, children[6] as ASTNode) } - + object ArgsSingle : ASTBuilder() { override fun build(children: List) = listOf(children[0] as ASTNode) } - + object ArgsMultiple : ASTBuilder() { override fun build(children: List) = (children[0] as List) + (children[2] as ASTNode) } @@ -767,12 +767,12 @@ object FirstFollowSets { val before = followSets[symbol]!!.size // ๋ณ€๊ฒฝ ์ „ FOLLOW ์ง‘ํ•ฉ ํฌ๊ธฐ val beta = prod.right.drop(i + 1) // ํ˜„์žฌ ์‹ฌ๋ณผ ์ดํ›„์˜ ์‹ฌ๋ณผ๋“ค val firstOfBeta = firstOfSequence(beta) // beta์˜ FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ - followSets[symbol]!!.addAll(firstOfBeta - TokenType.DOLLAR) // FIRST(beta)๋ฅผ FOLLOW(symbol)์— ์ถ”๊ฐ€ (epsilon ์ œ์™ธ) + followSets[symbol]!!.addAll(firstOfBeta - TokenType.DOLLAR) // FIRST(beta)๋ฅผ FOLLOW(tokenSymbol)์— ์ถ”๊ฐ€ (epsilon ์ œ์™ธ) logger.trace("FOLLOW({})์— FIRST({}) ์ถ”๊ฐ€. ํ˜„์žฌ: {}", symbol, beta, followSets[symbol]) // FOLLOW ์ง‘ํ•ฉ ์—…๋ฐ์ดํŠธ ๋กœ๊ทธ if (beta.isEmpty() || canDeriveEmpty(beta)) { // beta๊ฐ€ ๋น„์–ด์žˆ๊ฑฐ๋‚˜ epsilon์„ ํŒŒ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒฝ์šฐ - followSets[symbol]!!.addAll(followSets[prod.left]!!) // FOLLOW(์ƒ์‚ฐ ๊ทœ์น™ ์ขŒ๋ณ€)๋ฅผ FOLLOW(symbol)์— ์ถ”๊ฐ€ + followSets[symbol]!!.addAll(followSets[prod.left]!!) // FOLLOW(์ƒ์‚ฐ ๊ทœ์น™ ์ขŒ๋ณ€)๋ฅผ FOLLOW(tokenSymbol)์— ์ถ”๊ฐ€ logger.trace("FOLLOW({})์— FOLLOW({}) ์ถ”๊ฐ€. ํ˜„์žฌ: {}", symbol, prod.left, followSets[symbol]) // FOLLOW ์ง‘ํ•ฉ ์—…๋ฐ์ดํŠธ ๋กœ๊ทธ } @@ -902,17 +902,17 @@ object LRParserTable { private val compressedStates = mutableMapOf() // ์••์ถ•๋œ ์ƒํƒœ ์‹œ๊ทธ๋‹ˆ์ฒ˜ -> ์ƒํƒœ ID ๋งคํ•‘ private val conflicts = mutableListOf() // ํŒŒ์‹ฑ ์ถฉ๋Œ ๋ชฉ๋ก private val logger = LoggerFactory.getLogger(this::class.java) // ๋กœ๊ฑฐ ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ - + // Lazy initialization์„ ์œ„ํ•œ ํ”Œ๋ž˜๊ทธ๋“ค private var isInitialized = false private val stateCache = mutableMapOf, Int>() // ์ƒํƒœ ์บ์‹œ - + // 2D ๋ฐฐ์—ด ์ตœ์ ํ™”๋œ ํ…Œ์ด๋ธ”๋“ค private lateinit var actionTable2D: Array> // 2D ์•ก์…˜ ํ…Œ์ด๋ธ” [์ƒํƒœ][ํ† ํฐ] private lateinit var gotoTable2D: Array // 2D GOTO ํ…Œ์ด๋ธ” [์ƒํƒœ][๋…ผํ„ฐ๋ฏธ๋„] private val terminalToIndex = mutableMapOf() // ํ„ฐ๋ฏธ๋„ -> ์ธ๋ฑ์Šค ๋งคํ•‘ private val nonTerminalToIndex = mutableMapOf() // ๋…ผํ„ฐ๋ฏธ๋„ -> ์ธ๋ฑ์Šค ๋งคํ•‘ - + // ๋ฐฑ์—…์šฉ ๋งต ํ…Œ์ด๋ธ” (์ดˆ๊ธฐํ™” ์ค‘์—๋งŒ ์‚ฌ์šฉ) private val actionTable = mutableMapOf, LRAction>() // ์•ก์…˜ ํ…Œ์ด๋ธ” private val gotoTable = mutableMapOf, Int>() // GOTO ํ…Œ์ด๋ธ” @@ -952,18 +952,18 @@ object LRParserTable { */ private fun initializeTokenMappings() { logger.debug("ํ† ํฐ ๋งคํ•‘ ์ดˆ๊ธฐํ™” ์‹œ์ž‘") - + // ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ๋งคํ•‘ Grammar.terminals.forEachIndexed { index, terminal -> terminalToIndex[terminal] = index } - + // ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ๋งคํ•‘ Grammar.nonTerminals.forEachIndexed { index, nonTerminal -> nonTerminalToIndex[nonTerminal] = index } - - logger.debug("ํ† ํฐ ๋งคํ•‘ ์™„๋ฃŒ. ํ„ฐ๋ฏธ๋„: {}, ๋…ผํ„ฐ๋ฏธ๋„: {}", + + logger.debug("ํ† ํฐ ๋งคํ•‘ ์™„๋ฃŒ. ํ„ฐ๋ฏธ๋„: {}, ๋…ผํ„ฐ๋ฏธ๋„: {}", terminalToIndex.size, nonTerminalToIndex.size) } @@ -972,17 +972,17 @@ object LRParserTable { */ private fun build2DTables() { logger.debug("2D ํ…Œ์ด๋ธ” ๊ตฌ์ถ• ์‹œ์ž‘") - + val numStates = states.size val numTerminals = Grammar.terminals.size val numNonTerminals = Grammar.nonTerminals.size - + // ์•ก์…˜ ํ…Œ์ด๋ธ” 2D ๋ฐฐ์—ด ์ดˆ๊ธฐํ™” actionTable2D = Array(numStates) { arrayOfNulls(numTerminals) } - + // GOTO ํ…Œ์ด๋ธ” 2D ๋ฐฐ์—ด ์ดˆ๊ธฐํ™” (-1์€ ์—”ํŠธ๋ฆฌ ์—†์Œ์„ ์˜๋ฏธ) gotoTable2D = Array(numStates) { IntArray(numNonTerminals) { -1 } } - + // ๋งต์—์„œ 2D ๋ฐฐ์—ด๋กœ ๋ฐ์ดํ„ฐ ๋ณต์‚ฌ for ((key, action) in actionTable) { val (stateId, terminal) = key @@ -991,7 +991,7 @@ object LRParserTable { actionTable2D[stateId][terminalIndex] = action } } - + for ((key, nextState) in gotoTable) { val (stateId, nonTerminal) = key val nonTerminalIndex = nonTerminalToIndex[nonTerminal] @@ -999,8 +999,8 @@ object LRParserTable { gotoTable2D[stateId][nonTerminalIndex] = nextState } } - - logger.info("2D ํ…Œ์ด๋ธ” ๊ตฌ์ถ• ์™„๋ฃŒ. ์•ก์…˜ ํ…Œ์ด๋ธ”: {}x{}, GOTO ํ…Œ์ด๋ธ”: {}x{}", + + logger.info("2D ํ…Œ์ด๋ธ” ๊ตฌ์ถ• ์™„๋ฃŒ. ์•ก์…˜ ํ…Œ์ด๋ธ”: {}x{}, GOTO ํ…Œ์ด๋ธ”: {}x{}", numStates, numTerminals, numStates, numNonTerminals) } @@ -1036,7 +1036,7 @@ object LRParserTable { for ((symbol, itemSet) in transitions) { val newState = closure(itemSet) // ์ƒˆ๋กœ์šด ์ƒํƒœ์˜ ํด๋กœ์ € ๊ณ„์‚ฐ - + // ์บ์‹œ๋œ ์ƒํƒœ ํ™•์ธ val cachedStateId = stateCache[newState] val existingStateId = cachedStateId ?: stateMap[newState] // ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ƒํƒœ์ธ์ง€ ํ™•์ธ @@ -1051,10 +1051,10 @@ object LRParserTable { stateMap[newState] = newStateId // ๋งต์— ์ถ”๊ฐ€ stateCache[newState] = newStateId // ์บ์‹œ์— ์ถ”๊ฐ€ workList.add(newStateId) // ์ž‘์—… ๋ชฉ๋ก์— ์ถ”๊ฐ€ - + // ์••์ถ•๋œ ์ƒํƒœ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ์ƒ์„ฑ val coreSignature = generateCoreSignature(newState) - + // LALR ๋ณ‘ํ•ฉ ์‹œ๋„ val existingCoreStateId = compressedStates[coreSignature] if (existingCoreStateId != null && canMergeLALRStates(states[existingCoreStateId], newState)) { @@ -1065,13 +1065,13 @@ object LRParserTable { stateMap[mergedState] = existingCoreStateId stateCache[mergedState] = existingCoreStateId states.removeAt(states.size - 1) // ์ถ”๊ฐ€ํ–ˆ๋˜ ์ƒˆ ์ƒํƒœ ์ œ๊ฑฐ - - logger.debug("LALR ์ƒํƒœ ๋ณ‘ํ•ฉ: ์ƒํƒœ {}์™€ ๋ณ‘ํ•ฉ๋จ. ์‹œ๊ทธ๋‹ˆ์ฒ˜: {}", + + logger.debug("LALR ์ƒํƒœ ๋ณ‘ํ•ฉ: ์ƒํƒœ {}์™€ ๋ณ‘ํ•ฉ๋จ. ์‹œ๊ทธ๋‹ˆ์ฒ˜: {}", existingCoreStateId, coreSignature) existingCoreStateId } else { compressedStates[coreSignature] = newStateId - logger.debug("์ƒํƒœ {}์—์„œ ์‹ฌ๋ณผ {}๋กœ ์ „์ด: ์ƒˆ๋กœ์šด ์ƒํƒœ {} ์ƒ์„ฑ. ์‹œ๊ทธ๋‹ˆ์ฒ˜: {}", + logger.debug("์ƒํƒœ {}์—์„œ ์‹ฌ๋ณผ {}๋กœ ์ „์ด: ์ƒˆ๋กœ์šด ์ƒํƒœ {} ์ƒ์„ฑ. ์‹œ๊ทธ๋‹ˆ์ฒ˜: {}", stateId, symbol, newStateId, coreSignature) newStateId } @@ -1110,17 +1110,17 @@ object LRParserTable { // Core ์•„์ดํ…œ๋“ค (lookahead ์ œ์™ธ) ๋น„๊ต val core1 = state1.map { LRItem(it.production, it.dotPos, TokenType.DOLLAR) }.toSet() val core2 = state2.map { LRItem(it.production, it.dotPos, TokenType.DOLLAR) }.toSet() - + if (core1 != core2) { return false } - + // ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ง„ ์•„์ดํ…œ๋“ค์˜ lookahead ์ง‘ํ•ฉ์ด ๊ฒน์น˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธ val lookaheadMap1 = state1.groupBy { "${it.production.id}:${it.dotPos}" } .mapValues { it.value.map { item -> item.lookahead }.toSet() } val lookaheadMap2 = state2.groupBy { "${it.production.id}:${it.dotPos}" } .mapValues { it.value.map { item -> item.lookahead }.toSet() } - + // ๊ฐ core ์•„์ดํ…œ์— ๋Œ€ํ•ด lookahead ์ง‘ํ•ฉ์ด ๊ฒน์น˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธ for (coreKey in lookaheadMap1.keys) { val lookaheads1 = lookaheadMap1[coreKey] ?: emptySet() @@ -1130,7 +1130,7 @@ object LRParserTable { return false } } - + return true } @@ -1140,22 +1140,22 @@ object LRParserTable { */ private fun mergeLALRStates(state1: Set, state2: Set): Set { val mergedItems = mutableSetOf() - + // ๋ชจ๋“  ์•„์ดํ…œ๋“ค์„ core ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ™” val allItems = (state1 + state2).groupBy { "${it.production.id}:${it.dotPos}" } - + for ((coreKey, items) in allItems) { // ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ง„ ์•„์ดํ…œ๋“ค์˜ lookahead๋ฅผ ๋ชจ๋‘ ์ˆ˜์ง‘ val production = items.first().production val dotPos = items.first().dotPos val allLookaheads = items.map { it.lookahead }.toSet() - + // ๊ฐ lookahead์— ๋Œ€ํ•ด ๋ณ„๋„์˜ ์•„์ดํ…œ ์ƒ์„ฑ for (lookahead in allLookaheads) { mergedItems.add(LRItem(production, dotPos, lookahead)) } } - + return mergedItems } @@ -1226,12 +1226,12 @@ object LRParserTable { val resolved = resolveConflict(existing, LRAction.Reduce(item.production), item.lookahead, stateId) if (resolved != null) { actionTable[Pair(stateId, item.lookahead)] = resolved - logger.info("์ถฉ๋Œ ํ•ด๊ฒฐ๋จ: ์ƒํƒœ {}, ํ† ํฐ {}. ๊ธฐ์กด: {}, ์ƒˆ: Reduce({}), ํ•ด๊ฒฐ: {}", + logger.info("์ถฉ๋Œ ํ•ด๊ฒฐ๋จ: ์ƒํƒœ {}, ํ† ํฐ {}. ๊ธฐ์กด: {}, ์ƒˆ: Reduce({}), ํ•ด๊ฒฐ: {}", stateId, item.lookahead, existing, item.production, resolved) } else { // ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†๋Š” ์ถฉ๋Œ conflicts.add("Unresolvable conflict in state $stateId on ${item.lookahead}: $existing vs Reduce(${item.production})") - logger.warn("ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†๋Š” ์ถฉ๋Œ: ์ƒํƒœ {}, ํ† ํฐ {}. ๊ธฐ์กด: {}, ์ƒˆ: Reduce({})", + logger.warn("ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†๋Š” ์ถฉ๋Œ: ์ƒํƒœ {}, ํ† ํฐ {}. ๊ธฐ์กด: {}, ์ƒˆ: Reduce({})", stateId, item.lookahead, existing, item.production) } } else { @@ -1260,7 +1260,7 @@ object LRParserTable { stateId: Int ): LRAction? { logger.debug("์ถฉ๋Œ ํ•ด๊ฒฐ ์‹œ๋„: ์ƒํƒœ {}, ํ† ํฐ {}, ๊ธฐ์กด: {}, ์ƒˆ: {}", stateId, lookahead, existing, newAction) - + when { existing is LRAction.Shift && newAction is LRAction.Reduce -> { // Shift/Reduce ์ถฉ๋Œ @@ -1288,16 +1288,16 @@ object LRParserTable { ): LRAction? { val lookaheadPrec = OperatorPrecedenceTable.getPrecedence(lookahead) val productionPrec = getProductionPrecedence(reduceAction.production) - + logger.debug("Shift/Reduce ์ถฉ๋Œ ํ•ด๊ฒฐ: lookahead={}, precedence={}, production={}, precedence={}", lookahead, lookaheadPrec, reduceAction.production, productionPrec) - + if (lookaheadPrec == null || productionPrec == null) { // ์šฐ์„ ์ˆœ์œ„ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ Shift ์„ ํƒ (LR ํŒŒ์„œ์˜ ๊ธฐ๋ณธ ๋™์ž‘) logger.debug("์šฐ์„ ์ˆœ์œ„ ์ •๋ณด ์—†์Œ, Shift ์„ ํƒ") return shiftAction } - + return when { lookaheadPrec.precedence > productionPrec.precedence -> { logger.debug("Lookahead ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์Œ, Shift ์„ ํƒ") @@ -1338,7 +1338,7 @@ object LRParserTable { ): LRAction? { logger.debug("Reduce/Reduce ์ถฉ๋Œ ํ•ด๊ฒฐ: ๊ธฐ์กด={}, ์ƒˆ={}", existingReduce.production, newReduce.production) - + // ๋” ๊ธด ์ƒ์‚ฐ ๊ทœ์น™์„ ์„ ํƒ (๋” ๊ตฌ์ฒด์ ์ธ ๊ทœ์น™) return if (existingReduce.production.length >= newReduce.production.length) { logger.debug("๊ธฐ์กด ์ƒ์‚ฐ ๊ทœ์น™์ด ๋” ๊ธธ๊ฑฐ๋‚˜ ๊ฐ™์Œ, ๊ธฐ์กด ์„ ํƒ") @@ -1379,18 +1379,18 @@ object LRParserTable { ): LRAction { ensureInitialized() // Lazy initialization ๋ณด์žฅ logger.debug("getAction ํ˜ธ์ถœ๋จ. ์ƒํƒœ: {}, ํ„ฐ๋ฏธ๋„: {}", state, terminal) // getAction ํ˜ธ์ถœ ๋กœ๊ทธ - + val terminalIndex = terminalToIndex[terminal] if (terminalIndex == null || state >= actionTable2D.size || state < 0) { logger.error("์ž˜๋ชป๋œ ์ƒํƒœ ๋˜๋Š” ํ„ฐ๋ฏธ๋„: ์ƒํƒœ={}, ํ„ฐ๋ฏธ๋„={}", state, terminal) return LRAction.Error } - + val action = actionTable2D[state][terminalIndex] if (terminal == TokenType.DOLLAR) { logger.info("DOLLAR ํ† ํฐ ์•ก์…˜ ์กฐํšŒ: ์ƒํƒœ {}, ์•ก์…˜: {}", state, action) } - + if (action == null) { logger.error("์ƒํƒœ {}์—์„œ ํ† ํฐ {}์— ๋Œ€ํ•œ ์•ก์…˜์ด ์—†์Šต๋‹ˆ๋‹ค. ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ๋ฐœ์ƒ.", state, terminal) // ์•ก์…˜ ์—†์Œ ์—๋Ÿฌ ๋กœ๊ทธ logger.error("์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ์•ก์…˜๋“ค (์ƒํƒœ {}):", state) @@ -1401,7 +1401,7 @@ object LRParserTable { } } } - + logger.debug("getAction ๊ฒฐ๊ณผ: {}", action ?: LRAction.Error) // getAction ๊ฒฐ๊ณผ ๋กœ๊ทธ return action ?: LRAction.Error } @@ -1418,13 +1418,13 @@ object LRParserTable { ): Int? { ensureInitialized() // Lazy initialization ๋ณด์žฅ logger.debug("getGoto ํ˜ธ์ถœ๋จ. ์ƒํƒœ: {}, ๋…ผํ„ฐ๋ฏธ๋„: {}", state, nonTerminal) // getGoto ํ˜ธ์ถœ ๋กœ๊ทธ - + val nonTerminalIndex = nonTerminalToIndex[nonTerminal] if (nonTerminalIndex == null || state >= gotoTable2D.size || state < 0) { logger.debug("์ž˜๋ชป๋œ ์ƒํƒœ ๋˜๋Š” ๋…ผํ„ฐ๋ฏธ๋„: ์ƒํƒœ={}, ๋…ผํ„ฐ๋ฏธ๋„={}", state, nonTerminal) return null } - + val nextState = gotoTable2D[state][nonTerminalIndex] val result = if (nextState == -1) null else nextState logger.debug("getGoto ๊ฒฐ๊ณผ: {}", result) // getGoto ๊ฒฐ๊ณผ ๋กœ๊ทธ @@ -1553,7 +1553,7 @@ class CalculatorLexer : Lexer { tokens.add(Token(type, id, start)) logger.debug("{} ํ† ํฐ ์ถ”๊ฐ€: '{}' (์œ„์น˜: {})", type, id, start) } - + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž input[pos] == '+' -> { tokens.add(Token(TokenType.PLUS, "+", pos++)) @@ -1579,7 +1579,7 @@ class CalculatorLexer : Lexer { tokens.add(Token(TokenType.MODULO, "%", pos++)) logger.debug("MODULO ํ† ํฐ ์ถ”๊ฐ€") } - + // ๊ด„ํ˜ธ ๋ฐ ๊ตฌ๋ถ„์ž input[pos] == '(' -> { tokens.add(Token(TokenType.LEFT_PAREN, "(", pos++)) @@ -2491,4 +2491,4 @@ data class StepResult( val executionTimeMs: Long = 0, ) -``` \ No newline at end of file +```" \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/policies/EvaluationPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/policies/EvaluationPolicy.kt new file mode 100644 index 00000000..758b9b05 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/policies/EvaluationPolicy.kt @@ -0,0 +1,419 @@ +package hs.kr.entrydsm.domain.evaluator.policies + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.evaluator.entities.EvaluationContext +import hs.kr.entrydsm.domain.evaluator.entities.MathFunction +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ํ‘œํ˜„์‹ ํ‰๊ฐ€ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Policy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ‘œํ˜„์‹ ํ‰๊ฐ€ ๊ณผ์ •์—์„œ ์ ์šฉ๋˜๋Š” + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™๊ณผ ์ •์ฑ…์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ๋ณด์•ˆ, ์„ฑ๋Šฅ, ์ •ํ™•์„ฑ๊ณผ ๊ด€๋ จ๋œ + * ํ‰๊ฐ€ ์ •์ฑ…์„ ์ค‘์•™ ์ง‘์ค‘์‹์œผ๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Policy( + name = "Evaluation", + description = "ํ‘œํ˜„์‹ ํ‰๊ฐ€ ๊ณผ์ •์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™๊ณผ ์ •์ฑ…์„ ๊ด€๋ฆฌ", + domain = "evaluator", + scope = Scope.DOMAIN +) +class EvaluationPolicy { + + companion object { + private const val DEFAULT_MAX_DEPTH = 100 + private const val DEFAULT_MAX_NODES = 10000 + private const val DEFAULT_MAX_VARIABLES = 1000 + private const val DEFAULT_MAX_EXECUTION_TIME_MS = 30000L + private const val DEFAULT_MAX_MEMORY_MB = 100 + + // ํ—ˆ์šฉ๋œ ํ•จ์ˆ˜๋“ค (๋ณด์•ˆ์ƒ ์ œํ•œ) + private val ALLOWED_FUNCTIONS = setOf( + "ABS", "SQRT", "ROUND", "MIN", "MAX", "SUM", "AVG", "AVERAGE", + "IF", "POW", "LOG", "LOG10", "EXP", "SIN", "COS", "TAN", + "ASIN", "ACOS", "ATAN", "ATAN2", "SINH", "COSH", "TANH", + "ASINH", "ACOSH", "ATANH", "FLOOR", "CEIL", "CEILING", + "TRUNCATE", "TRUNC", "SIGN", "RANDOM", "RAND", "RADIANS", + "DEGREES", "PI", "E", "MOD", "GCD", "LCM", "FACTORIAL", + "COMBINATION", "COMB", "PERMUTATION", "PERM" + ) + + // ํ—ˆ์šฉ๋œ ์—ฐ์‚ฐ์ž๋“ค + private val ALLOWED_OPERATORS = setOf( + "+", "-", "*", "/", "%", "^", + "==", "!=", "<", "<=", ">", ">=", + "&&", "||", "!" + ) + + // ๊ธˆ์ง€๋œ ๋ณ€์ˆ˜ ์ด๋ฆ„๋“ค (์˜ˆ์•ฝ์–ด) + private val FORBIDDEN_VARIABLE_NAMES = setOf( + "null", "undefined", "true", "false", "NaN", "Infinity", + "eval", "function", "var", "let", "const", "class", "interface" + ) + } + + /** + * AST ๋…ธ๋“œ๊ฐ€ ํ‰๊ฐ€ ๊ฐ€๋Šฅํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ + * @return ํ‰๊ฐ€ ๊ฐ€๋Šฅํ•˜๋ฉด true + * @throws EvaluatorException ํ‰๊ฐ€ ์ •์ฑ… ๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun canEvaluate(node: ASTNode, context: EvaluationContext): Boolean { + return try { + if (!validateDepth(node, context.maxDepth)) { + throw EvaluatorException.evaluationDepthExceeded(context.maxDepth, calculateDepth(node)) + } + if (!validateNodeCount(node, DEFAULT_MAX_NODES)) { + throw EvaluatorException.evaluationComplexityExceeded(DEFAULT_MAX_NODES, countNodes(node)) + } + if (!validateFunctions(node)) { + throw EvaluatorException.securityViolation("ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ํ•จ์ˆ˜ ์‚ฌ์šฉ") + } + if (!validateOperators(node)) { + throw EvaluatorException.securityViolation("ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ") + } + if (!validateVariables(node, context)) { + throw EvaluatorException.securityViolation("์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜ ์‚ฌ์šฉ") + } + true + } catch (e: EvaluatorException) { + throw e + } catch (e: Exception) { + throw EvaluatorException.evaluationError(e) + } + } + + /** + * ํ‘œํ˜„์‹์˜ ๋ณด์•ˆ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ๋ณด์•ˆ ์ •์ฑ…์„ ๋งŒ์กฑํ•˜๋ฉด true + */ + fun validateSecurity(node: ASTNode): Boolean { + return validateFunctions(node) && + validateOperators(node) && + !containsSuspiciousPatterns(node) + } + + /** + * ์„ฑ๋Šฅ ์ •์ฑ…์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ + * @return ์„ฑ๋Šฅ ์ •์ฑ…์„ ๋งŒ์กฑํ•˜๋ฉด true + */ + fun validatePerformance(node: ASTNode, context: EvaluationContext): Boolean { + return validateDepth(node, context.maxDepth) && + validateNodeCount(node, DEFAULT_MAX_NODES) && + validateComplexity(node) + } + + /** + * ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ + * @return ์ถ”์ • ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ (MB) + */ + fun estimateMemoryUsage(node: ASTNode, context: EvaluationContext): Double { + val nodeCount = countNodes(node) + val variableCount = context.getVariableCount() + val baseMemory = (nodeCount * 0.1) + (variableCount * 0.05) // ๋งค์šฐ ๊ฐ„๋‹จํ•œ ์ถ”์ • + + return baseMemory + } + + /** + * ์‹คํ–‰ ์‹œ๊ฐ„์„ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @return ์ถ”์ • ์‹คํ–‰ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun estimateExecutionTime(node: ASTNode): Long { + val complexity = calculateComplexity(node) + return (complexity * 0.1).toLong() // ๋งค์šฐ ๊ฐ„๋‹จํ•œ ์ถ”์ • + } + + /** + * ํ•จ์ˆ˜ ์‚ฌ์šฉ์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param function ํ™•์ธํ•  ํ•จ์ˆ˜ + * @return ํ—ˆ์šฉ๋˜๋ฉด true + */ + fun isFunctionAllowed(function: MathFunction): Boolean { + return ALLOWED_FUNCTIONS.contains(function.name.uppercase()) + } + + /** + * ์—ฐ์‚ฐ์ž ์‚ฌ์šฉ์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ํ™•์ธํ•  ์—ฐ์‚ฐ์ž + * @return ํ—ˆ์šฉ๋˜๋ฉด true + */ + fun isOperatorAllowed(operator: String): Boolean { + return ALLOWED_OPERATORS.contains(operator) + } + + /** + * ๋ณ€์ˆ˜ ์ด๋ฆ„์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param variableName ํ™•์ธํ•  ๋ณ€์ˆ˜ ์ด๋ฆ„ + * @return ํ—ˆ์šฉ๋˜๋ฉด true + */ + fun isVariableNameAllowed(variableName: String): Boolean { + return variableName.isNotBlank() && + !FORBIDDEN_VARIABLE_NAMES.contains(variableName.lowercase()) && + variableName.matches(Regex("^[a-zA-Z_][a-zA-Z0-9_]*$")) + } + + /** + * ํ‰๊ฐ€ ๊ฒฐ๊ณผ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ๊ฒ€์ฆํ•  ๊ฒฐ๊ณผ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isValidResult(result: Any?): Boolean { + return when (result) { + null -> false + is Double -> !result.isNaN() && result.isFinite() + is Float -> !result.isNaN() && result.isFinite() + is Number -> true + is Boolean -> true + is String -> result.length < 10000 // ๋ฌธ์ž์—ด ๊ธธ์ด ์ œํ•œ + else -> false + } + } + + /** + * ์žฌ๊ท€ ๊นŠ์ด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateDepth(node: ASTNode, maxDepth: Int): Boolean { + fun calculateDepth(current: ASTNode, depth: Int = 0): Int { + if (depth > maxDepth) return depth + return current.getChildren().maxOfOrNull { calculateDepth(it, depth + 1) } ?: depth + } + + return calculateDepth(node) <= maxDepth + } + + /** + * ๋…ธ๋“œ ๊ฐœ์ˆ˜๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateNodeCount(node: ASTNode, maxNodes: Int): Boolean { + return countNodes(node) <= maxNodes + } + + /** + * ์‚ฌ์šฉ๋œ ํ•จ์ˆ˜๋“ค์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateFunctions(node: ASTNode): Boolean { + val usedFunctions = extractFunctions(node) + return usedFunctions.all { ALLOWED_FUNCTIONS.contains(it.uppercase()) } + } + + /** + * ์‚ฌ์šฉ๋œ ์—ฐ์‚ฐ์ž๋“ค์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateOperators(node: ASTNode): Boolean { + val usedOperators = extractOperators(node) + return usedOperators.all { ALLOWED_OPERATORS.contains(it) } + } + + /** + * ์‚ฌ์šฉ๋œ ๋ณ€์ˆ˜๋“ค์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateVariables(node: ASTNode, context: EvaluationContext): Boolean { + val usedVariables = node.getVariables() + return usedVariables.all { isVariableNameAllowed(it) && context.hasVariable(it) } + } + + /** + * ๋ณต์žก๋„๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateComplexity(node: ASTNode): Boolean { + val complexity = calculateComplexity(node) + return complexity < 10000 // ๋ณต์žก๋„ ์ œํ•œ + } + + /** + * ์˜์‹ฌ์Šค๋Ÿฌ์šด ํŒจํ„ด์ด ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * AST ๋…ธ๋“œ์˜ ๊ตฌ์กฐ๋ฅผ ์ง์ ‘ ๋ถ„์„ํ•˜์—ฌ ์œ„ํ—˜ํ•œ ๊ตฌ์„ฑ์š”์†Œ๋ฅผ ํƒ์ง€ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun containsSuspiciousPatterns(node: ASTNode): Boolean { + return when (node) { + is hs.kr.entrydsm.domain.ast.entities.FunctionCallNode -> { + // ์œ„ํ—˜ํ•œ ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฒ€์‚ฌ + val dangerousFunctions = setOf( + "eval", "exec", "system", "shell", "command", "run", + "import", "require", "load", "include", "file", "read", "write", + "delete", "remove", "create", "mkdir", "rmdir", "chmod" + ) + if (node.name.lowercase() in dangerousFunctions) { + return true + } + + // ๊ณผ๋„ํ•œ ์žฌ๊ท€ ํ˜ธ์ถœ ํŒจํ„ด ๊ฒ€์‚ฌ + val recursiveCallDepth = countRecursiveCalls(node, node.name) + if (recursiveCallDepth > 5) { + return true + } + + // ์ธ์ˆ˜ ๋‚ด์—์„œ๋„ ์žฌ๊ท€์ ์œผ๋กœ ๊ฒ€์‚ฌ + node.args.any { containsSuspiciousPatterns(it) } + } + is hs.kr.entrydsm.domain.ast.entities.VariableNode -> { + // ์œ„ํ—˜ํ•œ ๋ณ€์ˆ˜๋ช… ๊ฒ€์‚ฌ + val dangerousVariables = setOf( + "process", "runtime", "classloader", "system", "environment", + "__proto__", "constructor", "prototype", "global", "window" + ) + node.name.lowercase() in dangerousVariables + } + is hs.kr.entrydsm.domain.ast.entities.BinaryOpNode -> { + // ์ž์‹ ๋…ธ๋“œ๋“ค์„ ์žฌ๊ท€์ ์œผ๋กœ ๊ฒ€์‚ฌ + containsSuspiciousPatterns(node.left) || containsSuspiciousPatterns(node.right) + } + is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode -> { + // ์ž์‹ ๋…ธ๋“œ๋ฅผ ์žฌ๊ท€์ ์œผ๋กœ ๊ฒ€์‚ฌ + containsSuspiciousPatterns(node.operand) + } + is hs.kr.entrydsm.domain.ast.entities.IfNode -> { + // ์กฐ๊ฑด๋ฌธ์˜ ๋ชจ๋“  ๋ถ„๊ธฐ๋ฅผ ๊ฒ€์‚ฌ + containsSuspiciousPatterns(node.condition) || + containsSuspiciousPatterns(node.trueValue) || + containsSuspiciousPatterns(node.falseValue) + } + is hs.kr.entrydsm.domain.ast.entities.ArgumentsNode -> { + // ๋ชจ๋“  ์ธ์ˆ˜๋ฅผ ๊ฒ€์‚ฌ + node.arguments.any { containsSuspiciousPatterns(it) } + } + else -> { + // NumberNode, BooleanNode ๋“ฑ์€ ์ผ๋ฐ˜์ ์œผ๋กœ ์•ˆ์ „ + false + } + } + } + + /** + * ์žฌ๊ท€ ํ˜ธ์ถœ ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun countRecursiveCalls(node: ASTNode, functionName: String, depth: Int = 0): Int { + if (depth > 10) return depth // ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ + + return when (node) { + is hs.kr.entrydsm.domain.ast.entities.FunctionCallNode -> { + val currentDepth = if (node.name == functionName) depth + 1 else depth + val maxChildDepth = node.args.maxOfOrNull { + countRecursiveCalls(it, functionName, currentDepth) + } ?: currentDepth + maxChildDepth + } + else -> { + val children = node.getChildren() + children.maxOfOrNull { + countRecursiveCalls(it, functionName, depth) + } ?: depth + } + } + } + + /** + * ๋…ธ๋“œ ๊ฐœ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun countNodes(node: ASTNode): Int { + return 1 + node.getChildren().sumOf { countNodes(it) } + } + + /** + * ๋ณต์žก๋„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun calculateComplexity(node: ASTNode): Int { + // ๊ฐ„๋‹จํ•œ ๋ณต์žก๋„ ๊ณ„์‚ฐ: ๋…ธ๋“œ ๊ฐœ์ˆ˜ + ๊นŠ์ด * 2 + val nodeCount = countNodes(node) + val depth = calculateDepth(node) + return nodeCount + (depth * 2) + } + + /** + * AST ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun calculateDepth(node: ASTNode): Int { + return if (node.getChildren().isEmpty()) { + 1 + } else { + 1 + (node.getChildren().maxOfOrNull { calculateDepth(it) } ?: 0) + } + } + + /** + * ์‚ฌ์šฉ๋œ ํ•จ์ˆ˜๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + */ + private fun extractFunctions(node: ASTNode): Set { + // FunctionCallNode๋ฅผ ์ฐพ์•„์„œ ํ•จ์ˆ˜ ์ด๋ฆ„์„ ์ถ”์ถœ + return when (node) { + is hs.kr.entrydsm.domain.ast.entities.FunctionCallNode -> { + setOf(node.name) + node.args.flatMap { extractFunctions(it) }.toSet() + } + else -> { + node.getChildren().flatMap { extractFunctions(it) }.toSet() + } + } + } + + /** + * ์‚ฌ์šฉ๋œ ์—ฐ์‚ฐ์ž๋“ค์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + */ + private fun extractOperators(node: ASTNode): Set { + return when (node) { + is hs.kr.entrydsm.domain.ast.entities.BinaryOpNode -> { + setOf(node.operator) + extractOperators(node.left) + extractOperators(node.right) + } + is hs.kr.entrydsm.domain.ast.entities.UnaryOpNode -> { + setOf(node.operator) + extractOperators(node.operand) + } + else -> { + node.getChildren().flatMap { extractOperators(it) }.toSet() + } + } + } + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxDepth" to DEFAULT_MAX_DEPTH, + "maxNodes" to DEFAULT_MAX_NODES, + "maxVariables" to DEFAULT_MAX_VARIABLES, + "maxExecutionTimeMs" to DEFAULT_MAX_EXECUTION_TIME_MS, + "maxMemoryMB" to DEFAULT_MAX_MEMORY_MB, + "allowedFunctions" to ALLOWED_FUNCTIONS.size, + "allowedOperators" to ALLOWED_OPERATORS.size, + "securityEnabled" to true, + "performanceValidationEnabled" to true + ) + + /** + * ์ •์ฑ…์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "policyName" to "EvaluationPolicy", + "allowedFunctionCount" to ALLOWED_FUNCTIONS.size, + "allowedOperatorCount" to ALLOWED_OPERATORS.size, + "forbiddenVariableCount" to FORBIDDEN_VARIABLE_NAMES.size, + "securityRules" to listOf("function_whitelist", "operator_whitelist", "variable_validation"), + "performanceRules" to listOf("depth_limit", "node_limit", "complexity_limit") + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/policies/TypeCoercionPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/policies/TypeCoercionPolicy.kt new file mode 100644 index 00000000..ca88d8d6 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/policies/TypeCoercionPolicy.kt @@ -0,0 +1,373 @@ +package hs.kr.entrydsm.domain.evaluator.policies + +import hs.kr.entrydsm.domain.util.TypeUtils +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope +import kotlin.reflect.KClass + +/** + * ํƒ€์ž… ๊ฐ•์ œ ๋ณ€ํ™˜ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Policy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ‘œํ˜„์‹ ํ‰๊ฐ€ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š” + * ํƒ€์ž… ๋ณ€ํ™˜์˜ ๊ทœ์น™๊ณผ ์ •์ฑ…์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ์•ˆ์ „ํ•˜๊ณ  ์ผ๊ด€๋œ + * ํƒ€์ž… ๋ณ€ํ™˜์„ ๋ณด์žฅํ•˜๋ฉฐ, ํƒ€์ž… ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Policy( + name = "TypeCoercion", + description = "ํƒ€์ž… ๋ณ€ํ™˜๊ณผ ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ์„ ์œ„ํ•œ ์ •์ฑ…์œผ๋กœ ์•ˆ์ „ํ•œ ํƒ€์ž… ๊ฐ•์ œ๋ฅผ ๋‹ด๋‹น", + domain = "evaluator", + scope = Scope.DOMAIN +) +class TypeCoercionPolicy { + + companion object { + // ์ˆซ์ž ํƒ€์ž… ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์Œ) + private val NUMBER_TYPE_PRIORITY = mapOf( + Byte::class to 1, + Short::class to 2, + Int::class to 3, + Long::class to 4, + Float::class to 5, + Double::class to 6 + ) + + // ํ—ˆ์šฉ๋œ ํƒ€์ž…๋“ค + private val ALLOWED_TYPES = setOf( + Boolean::class, + Byte::class, + Short::class, + Int::class, + Long::class, + Float::class, + Double::class, + String::class, + List::class, + Map::class + ) + } + + /** + * ๊ฐ’์„ Double๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€ํ™˜ํ•  ๊ฐ’ + * @return ๋ณ€ํ™˜๋œ Double ๊ฐ’ + * @throws TypeCoercionException ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun toDouble(value: Any?): Double { + return when (value) { + null -> throw TypeCoercionException("null ๊ฐ’์€ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + is Double -> value + is Float -> value.toDouble() + is Long -> value.toDouble() + is Int -> value.toDouble() + is Short -> value.toDouble() + is Byte -> value.toDouble() + is String -> { + value.toDoubleOrNull() + ?: throw TypeCoercionException("๋ฌธ์ž์—ด '$value'๋ฅผ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + } + is Boolean -> if (value) 1.0 else 0.0 + else -> throw TypeCoercionException( + "ํƒ€์ž… ${value::class.simpleName}์„ Double๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $value" + ) + } + } + + /** + * ๊ฐ’์„ Boolean์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€ํ™˜ํ•  ๊ฐ’ + * @return ๋ณ€ํ™˜๋œ Boolean ๊ฐ’ + */ + fun toBoolean(value: Any?): Boolean { + return when (value) { + null -> false + is Boolean -> value + is Double -> value != 0.0 && !value.isNaN() + is Float -> value != 0.0f && !value.isNaN() + is Long -> value != 0L + is Int -> value != 0 + is Short -> value != 0.toShort() + is Byte -> value != 0.toByte() + is String -> value.isNotEmpty() && value.lowercase() !in setOf("false", "0", "null", "undefined") + is Collection<*> -> value.isNotEmpty() + is Map<*, *> -> value.isNotEmpty() + else -> true // ๊ธฐ๋ณธ์ ์œผ๋กœ null์ด ์•„๋‹Œ ๊ฐ์ฒด๋Š” true + } + } + + /** + * ๊ฐ’์„ Int๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€ํ™˜ํ•  ๊ฐ’ + * @return ๋ณ€ํ™˜๋œ Int ๊ฐ’ + * @throws TypeCoercionException ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun toInt(value: Any?): Int { + val doubleValue = toDouble(value) + if (doubleValue > Int.MAX_VALUE || doubleValue < Int.MIN_VALUE) { + throw TypeCoercionException("๊ฐ’ ${doubleValue}๊ฐ€ Int ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค") + } + return doubleValue.toInt() + } + + /** + * ๊ฐ’์„ Long์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€ํ™˜ํ•  ๊ฐ’ + * @return ๋ณ€ํ™˜๋œ Long ๊ฐ’ + * @throws TypeCoercionException ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun toLong(value: Any?): Long { + val doubleValue = toDouble(value) + if (doubleValue > Long.MAX_VALUE || doubleValue < Long.MIN_VALUE) { + throw TypeCoercionException("๊ฐ’ ${doubleValue}๊ฐ€ Long ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค") + } + return doubleValue.toLong() + } + + /** + * ๊ฐ’์„ String์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€ํ™˜ํ•  ๊ฐ’ + * @return ๋ณ€ํ™˜๋œ String ๊ฐ’ + */ + fun toString(value: Any?): String { + return when (value) { + null -> "null" + is String -> value + is Double -> { + if (value == value.toLong().toDouble()) { + value.toLong().toString() + } else { + value.toString() + } + } + is Float -> { + if (value == value.toLong().toFloat()) { + value.toLong().toString() + } else { + value.toString() + } + } + else -> value.toString() + } + } + + /** + * ๋‘ ๊ฐ’์ด ํƒ€์ž… ํ˜ธํ™˜์„ฑ์„ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param value1 ์ฒซ ๋ฒˆ์งธ ๊ฐ’ + * @param value2 ๋‘ ๋ฒˆ์งธ ๊ฐ’ + * @return ํ˜ธํ™˜๋˜๋ฉด true + */ + fun areCompatible(value1: Any?, value2: Any?): Boolean { + return try { + getCompatibleType(value1, value2) != null + } catch (e: TypeCoercionException) { + false + } + } + + /** + * ๋‘ ๊ฐ’์˜ ํ˜ธํ™˜ ๊ฐ€๋Šฅํ•œ ๊ณตํ†ต ํƒ€์ž…์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param value1 ์ฒซ ๋ฒˆ์งธ ๊ฐ’ + * @param value2 ๋‘ ๋ฒˆ์งธ ๊ฐ’ + * @return ๊ณตํ†ต ํƒ€์ž… + * @throws TypeCoercionException ํ˜ธํ™˜๋˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ + */ + fun getCompatibleType(value1: Any?, value2: Any?): KClass<*> { + val type1 = getEffectiveType(value1) + val type2 = getEffectiveType(value2) + + // ๊ฐ™์€ ํƒ€์ž…์ด๋ฉด ๊ทธ๋Œ€๋กœ ๋ฐ˜ํ™˜ + if (type1 == type2) { + return type1 + } + + // ๋‘˜ ๋‹ค ์ˆซ์ž ํƒ€์ž…์ธ ๊ฒฝ์šฐ + if (TypeUtils.isNumericType(type1) && TypeUtils.isNumericType(type2)) { + return getHigherPriorityNumericType(type1, type2) + } + + // Boolean๊ณผ ์ˆซ์ž ํƒ€์ž…์˜ ๊ฒฝ์šฐ + if ((type1 == Boolean::class && TypeUtils.isNumericType(type2)) || + (type2 == Boolean::class && TypeUtils.isNumericType(type1))) { + return Double::class // Boolean์€ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ + } + + // String๊ณผ ๋‹ค๋ฅธ ํƒ€์ž…์˜ ๊ฒฝ์šฐ + if (type1 == String::class || type2 == String::class) { + return String::class // ๋ชจ๋“  ํƒ€์ž…์€ String์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ + } + + throw TypeCoercionException( + "ํƒ€์ž… ${type1.simpleName}๊ณผ ${type2.simpleName}์€ ํ˜ธํ™˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + ) + } + + /** + * ๋‘ ๊ฐ’์„ ๊ณตํ†ต ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param value1 ์ฒซ ๋ฒˆ์งธ ๊ฐ’ + * @param value2 ๋‘ ๋ฒˆ์งธ ๊ฐ’ + * @return ๋ณ€ํ™˜๋œ ๊ฐ’๋“ค์˜ Pair + */ + fun coerceToCommonType(value1: Any?, value2: Any?): Pair { + val commonType = getCompatibleType(value1, value2) + + return when (commonType) { + Double::class -> Pair(toDouble(value1), toDouble(value2)) + Boolean::class -> Pair(toBoolean(value1), toBoolean(value2)) + String::class -> Pair(toString(value1), toString(value2)) + Int::class -> Pair(toInt(value1), toInt(value2)) + Long::class -> Pair(toLong(value1), toLong(value2)) + else -> throw TypeCoercionException( + "ํƒ€์ž… ${commonType.simpleName}๋กœ์˜ ๋ณ€ํ™˜์€ ์ง€์›๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + ) + } + } + + /** + * ํƒ€์ž…์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ํ™•์ธํ•  ๊ฐ’ + * @return ํ—ˆ์šฉ๋˜๋ฉด true + */ + fun isAllowedType(value: Any?): Boolean { + if (value == null) return true + return ALLOWED_TYPES.any { it.isInstance(value) } + } + + + /** + * ๊ฐ’์ด ์ˆซ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ํ™•์ธํ•  ๊ฐ’ + * @return ์ˆซ์ž์ด๋ฉด true + */ + fun isNumeric(value: Any?): Boolean { + return when (value) { + null -> false + is Number -> true + is String -> value.toDoubleOrNull() != null + is Boolean -> true // Boolean์€ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ + else -> false + } + } + + /** + * ๊ฐ’์ด ์ •์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ํ™•์ธํ•  ๊ฐ’ + * @return ์ •์ˆ˜์ด๋ฉด true + */ + fun isInteger(value: Any?): Boolean { + return when (value) { + is Byte, is Short, is Int, is Long -> true + is Double -> value == value.toLong().toDouble() + is Float -> value == value.toLong().toFloat() + is String -> { + val doubleValue = value.toDoubleOrNull() + doubleValue != null && doubleValue == doubleValue.toLong().toDouble() + } + else -> false + } + } + + /** + * ์•ˆ์ „ํ•œ ๋‚˜๋ˆ—์…ˆ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param dividend ํ”ผ์ œ์ˆ˜ + * @param divisor ์ œ์ˆ˜ + * @return ๋‚˜๋ˆ—์…ˆ ๊ฒฐ๊ณผ + * @throws TypeCoercionException 0์œผ๋กœ ๋‚˜๋ˆ„๋Š” ๊ฒฝ์šฐ + */ + fun safeDivide(dividend: Any?, divisor: Any?): Double { + val dividendNum = toDouble(dividend) + val divisorNum = toDouble(divisor) + + if (divisorNum == 0.0) { + throw TypeCoercionException("0์œผ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + } + + val result = dividendNum / divisorNum + if (!result.isFinite()) { + throw TypeCoercionException("๋‚˜๋ˆ—์…ˆ ๊ฒฐ๊ณผ๊ฐ€ ์œ ํ•œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $result") + } + + return result + } + + // Private helper methods + + /** + * ๊ฐ’์˜ ์‹ค์ œ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun getEffectiveType(value: Any?): KClass<*> { + return when (value) { + null -> Any::class + is Boolean -> Boolean::class + is Byte -> Byte::class + is Short -> Short::class + is Int -> Int::class + is Long -> Long::class + is Float -> Float::class + is Double -> Double::class + is String -> String::class + else -> value::class + } + } + + /** + * ๋” ๋†’์€ ์šฐ์„ ์ˆœ์œ„์˜ ์ˆซ์ž ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun getHigherPriorityNumericType(type1: KClass<*>, type2: KClass<*>): KClass<*> { + val priority1 = NUMBER_TYPE_PRIORITY[type1] ?: 0 + val priority2 = NUMBER_TYPE_PRIORITY[type2] ?: 0 + + return if (priority1 >= priority2) type1 else type2 + } + + /** + * ํƒ€์ž… ๊ฐ•์ œ ๋ณ€ํ™˜ ์˜ˆ์™ธ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + class TypeCoercionException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "allowedTypes" to ALLOWED_TYPES.map { it.simpleName }, + "numericTypePriority" to NUMBER_TYPE_PRIORITY.mapKeys { it.key.simpleName }, + "strictMode" to false, + "allowBooleanToNumber" to true, + "allowStringToNumber" to true, + "allowImplicitConversion" to true + ) + + /** + * ์ •์ฑ…์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "policyName" to "TypeCoercionPolicy", + "supportedTypes" to ALLOWED_TYPES.size, + "numericTypes" to NUMBER_TYPE_PRIORITY.size, + "conversionRules" to listOf( + "numeric_promotion", + "boolean_to_numeric", + "any_to_string", + "safe_division" + ) + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/registries/FunctionRegistry.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/registries/FunctionRegistry.kt new file mode 100644 index 00000000..2c94cbf5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/registries/FunctionRegistry.kt @@ -0,0 +1,135 @@ +package hs.kr.entrydsm.domain.evaluator.registries + +import hs.kr.entrydsm.domain.evaluator.interfaces.FunctionEvaluator +import hs.kr.entrydsm.domain.evaluator.functions.* + +/** + * ํ•จ์ˆ˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋ชจ๋“  ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ํ•จ์ˆ˜๋“ค์„ ๋“ฑ๋กํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋ฉฐ, + * ํ•จ์ˆ˜๋ช…์œผ๋กœ ํ•จ์ˆ˜ ํ‰๊ฐ€๊ธฐ๋ฅผ ์กฐํšŒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.06 + */ +class FunctionRegistry { + + private val functions = mutableMapOf() + + init { + registerDefaultFunctions() + } + + /** + * ๊ธฐ๋ณธ ํ•จ์ˆ˜๋“ค์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + */ + private fun registerDefaultFunctions() { + // ๊ธฐ๋ณธ ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค + register(AbsFunction()) + register(SqrtFunction()) + register(RoundFunction()) + register(MinFunction()) + register(MaxFunction()) + register(SumFunction()) + register(AvgFunction()) + register(AverageFunction()) + register(IfFunction()) + register(PowFunction()) + + // ์‚ผ๊ฐํ•จ์ˆ˜๋“ค + register(SinFunction()) + register(CosFunction()) + register(TanFunction()) + + // ๋กœ๊ทธ ํ•จ์ˆ˜๋“ค + register(LogFunction()) + register(Log10Function()) + register(ExpFunction()) + } + + /** + * ํ•จ์ˆ˜๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param evaluator ๋“ฑ๋กํ•  ํ•จ์ˆ˜ ํ‰๊ฐ€๊ธฐ + */ + fun register(evaluator: FunctionEvaluator) { + functions[evaluator.getFunctionName().uppercase()] = evaluator + } + + /** + * ํ•จ์ˆ˜๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @return ํ•จ์ˆ˜ ํ‰๊ฐ€๊ธฐ ๋˜๋Š” null + */ + fun get(name: String): FunctionEvaluator? { + return functions[name.uppercase()] + } + + /** + * ํ•จ์ˆ˜๊ฐ€ ๋“ฑ๋ก๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ํ•จ์ˆ˜๋ช… + * @return ๋“ฑ๋ก๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun contains(name: String): Boolean { + return name.uppercase() in functions + } + + /** + * ๋“ฑ๋ก๋œ ๋ชจ๋“  ํ•จ์ˆ˜๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•จ์ˆ˜๋ช… ์ง‘ํ•ฉ + */ + fun getAllFunctionNames(): Set { + return functions.keys.toSet() + } + + /** + * ๋“ฑ๋ก๋œ ํ•จ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•จ์ˆ˜ ๊ฐœ์ˆ˜ + */ + fun size(): Int = functions.size + + /** + * ํ•จ์ˆ˜๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์ œ๊ฑฐํ•  ํ•จ์ˆ˜๋ช… + * @return ์ œ๊ฑฐ๋œ ํ•จ์ˆ˜ ํ‰๊ฐ€๊ธฐ ๋˜๋Š” null + */ + fun unregister(name: String): FunctionEvaluator? { + return functions.remove(name.uppercase()) + } + + /** + * ๋ชจ๋“  ํ•จ์ˆ˜๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun clear() { + functions.clear() + } + + /** + * ํ•จ์ˆ˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•จ์ˆ˜ ์ •๋ณด ๋งต + */ + fun getFunctionInfo(): Map> { + return functions.mapValues { (_, evaluator) -> + mapOf( + "name" to evaluator.getFunctionName(), + "description" to evaluator.getDescription(), + "supportedArgumentCounts" to (evaluator.getSupportedArgumentCounts() ?: listOf("variable")) + ) + } + } + + companion object { + /** + * ๊ธฐ๋ณธ ํ•จ์ˆ˜ ๋ ˆ์ง€์ŠคํŠธ๋ฆฌ ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return FunctionRegistry ์ธ์Šคํ„ด์Šค + */ + fun createDefault(): FunctionRegistry = FunctionRegistry() + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/services/MathFunctionService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/services/MathFunctionService.kt new file mode 100644 index 00000000..9d77c3c1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/services/MathFunctionService.kt @@ -0,0 +1,549 @@ +package hs.kr.entrydsm.domain.evaluator.services + +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType +import hs.kr.entrydsm.global.configuration.EvaluatorConfiguration +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider +import kotlin.math.PI +import kotlin.math.abs +import kotlin.math.acos +import kotlin.math.acosh +import kotlin.math.asin +import kotlin.math.asinh +import kotlin.math.atan +import kotlin.math.atan2 +import kotlin.math.atanh +import kotlin.math.ceil +import kotlin.math.cos +import kotlin.math.cosh +import kotlin.math.exp +import kotlin.math.floor +import kotlin.math.ln +import kotlin.math.log10 +import kotlin.math.log2 +import kotlin.math.pow +import kotlin.math.round +import kotlin.math.sign +import kotlin.math.sin +import kotlin.math.sinh +import kotlin.math.sqrt +import kotlin.math.tan +import kotlin.math.tanh + +/** + * ์ˆ˜ํ•™ ํ•จ์ˆ˜ ์‹คํ–‰์„ ๋‹ด๋‹นํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋‹ค์–‘ํ•œ ์ˆ˜ํ•™ ํ•จ์ˆ˜์˜ ์‹คํ–‰ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๋ฉฐ, + * ํ•จ์ˆ˜๋ณ„ ์ธ์ˆ˜ ๊ฒ€์ฆ๊ณผ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Service( + name = "์ˆ˜ํ•™ ํ•จ์ˆ˜ ์‹คํ–‰ ์„œ๋น„์Šค", + type = ServiceType.DOMAIN_SERVICE +) +class MathFunctionService( + private val configurationProvider: ConfigurationProvider? = null +) { + + // ์„ค์ •์€ ConfigurationProvider๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ์ ‘๊ทผ (๊ธฐ๋ณธ๊ฐ’ ์‚ฌ์šฉ ๊ฐ€๋Šฅ) + private val config: EvaluatorConfiguration + get() = configurationProvider?.getEvaluatorConfiguration() ?: EvaluatorConfiguration() + + /** + * ์ˆ˜ํ•™ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param functionName ํ•จ์ˆ˜๋ช… + * @param arguments ์ธ์ˆ˜ ๋ชฉ๋ก + * @return ์‹คํ–‰ ๊ฒฐ๊ณผ + */ + fun executeFunction(functionName: String, arguments: List): Any? { + val funcName = functionName.uppercase() + + return when (funcName) { + // ๊ธฐ๋ณธ ์ˆ˜ํ•™ ํ•จ์ˆ˜๋“ค + "ABS" -> executeAbs(arguments) + "SQRT" -> executeSqrt(arguments) + "ROUND" -> executeRound(arguments) + "FLOOR" -> executeFloor(arguments) + "CEIL" -> executeCeil(arguments) + "SIGN" -> executeSign(arguments) + + // ์‚ผ๊ฐํ•จ์ˆ˜ + "SIN" -> executeSin(arguments) + "COS" -> executeCos(arguments) + "TAN" -> executeTan(arguments) + "ASIN" -> executeAsin(arguments) + "ACOS" -> executeAcos(arguments) + "ATAN" -> executeAtan(arguments) + "ATAN2" -> executeAtan2(arguments) + + // ์Œ๊ณกํ•จ์ˆ˜ + "SINH" -> executeSinh(arguments) + "COSH" -> executeCosh(arguments) + "TANH" -> executeTanh(arguments) + "ASINH" -> executeAsinh(arguments) + "ACOSH" -> executeAcosh(arguments) + "ATANH" -> executeAtanh(arguments) + + // ์ง€์ˆ˜ ๋ฐ ๋กœ๊ทธ ํ•จ์ˆ˜ + "EXP" -> executeExp(arguments) + "LOG" -> executeLog(arguments) + "LOG10" -> executeLog10(arguments) + "LOG2" -> executeLog2(arguments) + "POW" -> executePow(arguments) + + // ํ†ต๊ณ„ ํ•จ์ˆ˜ + "MIN" -> executeMin(arguments) + "MAX" -> executeMax(arguments) + "SUM" -> executeSum(arguments) + "AVG", "AVERAGE" -> executeAverage(arguments) + "COUNT" -> executeCount(arguments) + + // ์กฐํ•ฉ ๋ฐ ํŒฉํ† ๋ฆฌ์–ผ + "FACTORIAL" -> executeFactorial(arguments) + "COMBINATION" -> executeCombination(arguments) + "PERMUTATION" -> executePermutation(arguments) + + // ์กฐ๊ฑด ํ•จ์ˆ˜ + "IF" -> executeIf(arguments) + + // ๋ณ€ํ™˜ ํ•จ์ˆ˜ + "RADIANS" -> executeRadians(arguments) + "DEGREES" -> executeDegrees(arguments) + + // ๊ธฐํƒ€ ํ•จ์ˆ˜ + "RANDOM" -> executeRandom(arguments) + "GCD" -> executeGcd(arguments) + "LCM" -> executeLcm(arguments) + + else -> throw EvaluatorException.unsupportedFunction(funcName) + } + } + + /** + * ์ ˆ๋Œ“๊ฐ’ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeAbs(arguments: List): Double { + validateArgumentCount("ABS", arguments, 1) + return abs(toDouble(arguments[0])) + } + + /** + * ์ œ๊ณฑ๊ทผ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeSqrt(arguments: List): Double { + validateArgumentCount("SQRT", arguments, 1) + val value = toDouble(arguments[0]) + if (value < 0) throw EvaluatorException.mathError("์Œ์ˆ˜์˜ ์ œ๊ณฑ๊ทผ์„ ๊ณ„์‚ฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + return sqrt(value) + } + + /** + * ๋ฐ˜์˜ฌ๋ฆผ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeRound(arguments: List): Double { + return when (arguments.size) { + 1 -> round(toDouble(arguments[0])) + 2 -> { + val value = toDouble(arguments[0]) + val places = toDouble(arguments[1]).toInt() + val multiplier = 10.0.pow(places.toDouble()) + round(value * multiplier) / multiplier + } + else -> throw EvaluatorException.wrongArgumentCount("ROUND", 1, arguments.size) + } + } + + /** + * ๋ฐ”๋‹ฅ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeFloor(arguments: List): Double { + validateArgumentCount("FLOOR", arguments, 1) + return floor(toDouble(arguments[0])) + } + + /** + * ์ฒœ์žฅ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeCeil(arguments: List): Double { + validateArgumentCount("CEIL", arguments, 1) + return ceil(toDouble(arguments[0])) + } + + /** + * ๋ถ€ํ˜ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeSign(arguments: List): Double { + validateArgumentCount("SIGN", arguments, 1) + return sign(toDouble(arguments[0])) + } + + /** + * ์‚ฌ์ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeSin(arguments: List): Double { + validateArgumentCount("SIN", arguments, 1) + return sin(toDouble(arguments[0])) + } + + /** + * ์ฝ”์‚ฌ์ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeCos(arguments: List): Double { + validateArgumentCount("COS", arguments, 1) + return cos(toDouble(arguments[0])) + } + + /** + * ํƒ„์  ํŠธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeTan(arguments: List): Double { + validateArgumentCount("TAN", arguments, 1) + return tan(toDouble(arguments[0])) + } + + /** + * ์•„ํฌ์‚ฌ์ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeAsin(arguments: List): Double { + validateArgumentCount("ASIN", arguments, 1) + val value = toDouble(arguments[0]) + if (value < -1 || value > 1) throw EvaluatorException.mathError("ASIN์˜ ์ •์˜์—ญ์€ [-1, 1]์ž…๋‹ˆ๋‹ค") + return asin(value) + } + + /** + * ์•„ํฌ์ฝ”์‚ฌ์ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeAcos(arguments: List): Double { + validateArgumentCount("ACOS", arguments, 1) + val value = toDouble(arguments[0]) + if (value < -1 || value > 1) throw EvaluatorException.mathError("ACOS์˜ ์ •์˜์—ญ์€ [-1, 1]์ž…๋‹ˆ๋‹ค") + return acos(value) + } + + /** + * ์•„ํฌํƒ„์  ํŠธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeAtan(arguments: List): Double { + validateArgumentCount("ATAN", arguments, 1) + return atan(toDouble(arguments[0])) + } + + /** + * 2์ธ์ˆ˜ ์•„ํฌํƒ„์  ํŠธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeAtan2(arguments: List): Double { + validateArgumentCount("ATAN2", arguments, 2) + return atan2(toDouble(arguments[0]), toDouble(arguments[1])) + } + + /** + * ์Œ๊ณก์‚ฌ์ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeSinh(arguments: List): Double { + validateArgumentCount("SINH", arguments, 1) + return sinh(toDouble(arguments[0])) + } + + /** + * ์Œ๊ณก์ฝ”์‚ฌ์ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeCosh(arguments: List): Double { + validateArgumentCount("COSH", arguments, 1) + return cosh(toDouble(arguments[0])) + } + + /** + * ์Œ๊ณกํƒ„์  ํŠธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeTanh(arguments: List): Double { + validateArgumentCount("TANH", arguments, 1) + return tanh(toDouble(arguments[0])) + } + + /** + * ์Œ๊ณก์•„ํฌ์‚ฌ์ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeAsinh(arguments: List): Double { + validateArgumentCount("ASINH", arguments, 1) + return asinh(toDouble(arguments[0])) + } + + /** + * ์Œ๊ณก์•„ํฌ์ฝ”์‚ฌ์ธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeAcosh(arguments: List): Double { + validateArgumentCount("ACOSH", arguments, 1) + val value = toDouble(arguments[0]) + if (value < 1) throw EvaluatorException.mathError("ACOSH์˜ ์ •์˜์—ญ์€ [1, โˆž)์ž…๋‹ˆ๋‹ค") + return acosh(value) + } + + /** + * ์Œ๊ณก์•„ํฌํƒ„์  ํŠธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeAtanh(arguments: List): Double { + validateArgumentCount("ATANH", arguments, 1) + val value = toDouble(arguments[0]) + if (value <= -1 || value >= 1) throw EvaluatorException.mathError("ATANH์˜ ์ •์˜์—ญ์€ (-1, 1)์ž…๋‹ˆ๋‹ค") + return atanh(value) + } + + /** + * ์ง€์ˆ˜ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeExp(arguments: List): Double { + validateArgumentCount("EXP", arguments, 1) + return exp(toDouble(arguments[0])) + } + + /** + * ์ž์—ฐ๋กœ๊ทธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeLog(arguments: List): Double { + validateArgumentCount("LOG", arguments, 1) + val value = toDouble(arguments[0]) + if (value <= 0) throw EvaluatorException.mathError("์–‘์ˆ˜์— ๋Œ€ํ•ด์„œ๋งŒ ๋กœ๊ทธ๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค") + return ln(value) + } + + /** + * ์ƒ์šฉ๋กœ๊ทธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeLog10(arguments: List): Double { + validateArgumentCount("LOG10", arguments, 1) + val value = toDouble(arguments[0]) + if (value <= 0) throw EvaluatorException.mathError("์–‘์ˆ˜์— ๋Œ€ํ•ด์„œ๋งŒ ๋กœ๊ทธ๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค") + return log10(value) + } + + /** + * ์ด์ง„๋กœ๊ทธ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeLog2(arguments: List): Double { + validateArgumentCount("LOG2", arguments, 1) + val value = toDouble(arguments[0]) + if (value <= 0) throw EvaluatorException.mathError("์–‘์ˆ˜์— ๋Œ€ํ•ด์„œ๋งŒ ๋กœ๊ทธ๋ฅผ ๊ณ„์‚ฐํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค") + return log2(value) + } + + /** + * ๊ฑฐ๋“ญ์ œ๊ณฑ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executePow(arguments: List): Double { + validateArgumentCount("POW", arguments, 2) + return toDouble(arguments[0]).pow(toDouble(arguments[1])) + } + + /** + * ์ตœ์†Ÿ๊ฐ’ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeMin(arguments: List): Double { + if (arguments.isEmpty()) throw EvaluatorException.wrongArgumentCount("MIN", 1, arguments.size) + return arguments.map { toDouble(it) }.minOrNull() ?: 0.0 + } + + /** + * ์ตœ๋Œ“๊ฐ’ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeMax(arguments: List): Double { + if (arguments.isEmpty()) throw EvaluatorException.wrongArgumentCount("MAX", 1, arguments.size) + return arguments.map { toDouble(it) }.maxOrNull() ?: 0.0 + } + + /** + * ํ•ฉ๊ณ„ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeSum(arguments: List): Double { + return arguments.map { toDouble(it) }.sum() + } + + /** + * ํ‰๊ท  ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeAverage(arguments: List): Double { + if (arguments.isEmpty()) throw EvaluatorException.wrongArgumentCount("AVG", 1, arguments.size) + return arguments.map { toDouble(it) }.average() + } + + /** + * ๊ฐœ์ˆ˜ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeCount(arguments: List): Double { + return arguments.size.toDouble() + } + + /** + * ํŒฉํ† ๋ฆฌ์–ผ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeFactorial(arguments: List): Double { + validateArgumentCount("FACTORIAL", arguments, 1) + val n = toDouble(arguments[0]).toInt() + if (n < 0) throw EvaluatorException.mathError("์Œ์ˆ˜์˜ ํŒฉํ† ๋ฆฌ์–ผ์€ ์ •์˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") + if (n > 170) throw EvaluatorException.mathError("ํŒฉํ† ๋ฆฌ์–ผ ๊ฐ’์ด ๋„ˆ๋ฌด ํฝ๋‹ˆ๋‹ค") + + var result = 1.0 + for (i in 1..n) { + result *= i + } + return result + } + + /** + * ์กฐํ•ฉ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeCombination(arguments: List): Double { + validateArgumentCount("COMBINATION", arguments, 2) + val n = toDouble(arguments[0]).toInt() + val k = toDouble(arguments[1]).toInt() + + if (n < 0 || k < 0) throw EvaluatorException.mathError("์Œ์ˆ˜์— ๋Œ€ํ•œ ์กฐํ•ฉ์€ ์ •์˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") + if (k > n) return 0.0 + + return executeFactorial(listOf(n)) / (executeFactorial(listOf(k)) * executeFactorial(listOf(n - k))) + } + + /** + * ์ˆœ์—ด ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executePermutation(arguments: List): Double { + validateArgumentCount("PERMUTATION", arguments, 2) + val n = toDouble(arguments[0]).toInt() + val k = toDouble(arguments[1]).toInt() + + if (n < 0 || k < 0) throw EvaluatorException.mathError("์Œ์ˆ˜์— ๋Œ€ํ•œ ์ˆœ์—ด์€ ์ •์˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") + if (k > n) return 0.0 + + return executeFactorial(listOf(n)) / executeFactorial(listOf(n - k)) + } + + /** + * ์กฐ๊ฑด ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeIf(arguments: List): Any? { + validateArgumentCount("IF", arguments, 3) + val condition = toBoolean(arguments[0]) + return if (condition) arguments[1] else arguments[2] + } + + /** + * ๋ผ๋””์•ˆ ๋ณ€ํ™˜ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeRadians(arguments: List): Double { + validateArgumentCount("RADIANS", arguments, 1) + return toDouble(arguments[0]) * PI / 180.0 + } + + /** + * ๋„ ๋ณ€ํ™˜ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeDegrees(arguments: List): Double { + validateArgumentCount("DEGREES", arguments, 1) + return toDouble(arguments[0]) * 180.0 / PI + } + + /** + * ๋žœ๋ค ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeRandom(arguments: List): Double { + return when (arguments.size) { + 0 -> kotlin.random.Random.nextDouble() + 1 -> kotlin.random.Random.nextDouble(toDouble(arguments[0])) + 2 -> kotlin.random.Random.nextDouble(toDouble(arguments[0]), toDouble(arguments[1])) + else -> throw EvaluatorException.wrongArgumentCount("RANDOM", 0, arguments.size) + } + } + + /** + * ์ตœ๋Œ€๊ณต์•ฝ์ˆ˜ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeGcd(arguments: List): Double { + validateArgumentCount("GCD", arguments, 2) + val a = toDouble(arguments[0]).toInt() + val b = toDouble(arguments[1]).toInt() + + return gcd(a, b).toDouble() + } + + /** + * ์ตœ์†Œ๊ณต๋ฐฐ์ˆ˜ ํ•จ์ˆ˜๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeLcm(arguments: List): Double { + validateArgumentCount("LCM", arguments, 2) + val a = toDouble(arguments[0]).toInt() + val b = toDouble(arguments[1]).toInt() + + return (a * b / gcd(a, b)).toDouble() + } + + /** + * ์ตœ๋Œ€๊ณต์•ฝ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun gcd(a: Int, b: Int): Int { + return if (b == 0) abs(a) else gcd(b, a % b) + } + + /** + * ์ธ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateArgumentCount(functionName: String, arguments: List, expected: Int) { + if (arguments.size != expected) { + throw EvaluatorException.wrongArgumentCount(functionName, expected, arguments.size) + } + } + + /** + * ๊ฐ’์„ Double๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun toDouble(value: Any?): Double { + return when (value) { + is Double -> value + is Int -> value.toDouble() + is Float -> value.toDouble() + is Long -> value.toDouble() + is String -> value.toDoubleOrNull() ?: throw EvaluatorException.numberConversionError(value) + else -> throw EvaluatorException.unsupportedType(value?.javaClass?.simpleName ?: "null", value) + } + } + + /** + * ๊ฐ’์„ Boolean์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun toBoolean(value: Any?): Boolean { + return when (value) { + is Boolean -> value + is Double -> value != 0.0 + is Int -> value != 0 + is String -> value.lowercase() in setOf("true", "1", "yes", "on") + else -> false + } + } + + /** + * ์ง€์›๋˜๋Š” ํ•จ์ˆ˜ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getSupportedFunctions(): Set { + return setOf( + "ABS", "SQRT", "ROUND", "FLOOR", "CEIL", "SIGN", + "SIN", "COS", "TAN", "ASIN", "ACOS", "ATAN", "ATAN2", + "SINH", "COSH", "TANH", "ASINH", "ACOSH", "ATANH", + "EXP", "LOG", "LOG10", "LOG2", "POW", + "MIN", "MAX", "SUM", "AVG", "AVERAGE", "COUNT", + "FACTORIAL", "COMBINATION", "PERMUTATION", + "IF", "RADIANS", "DEGREES", "RANDOM", "GCD", "LCM" + ) + } + + /** + * ํ•จ์ˆ˜๊ฐ€ ์ง€์›๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isSupported(functionName: String): Boolean { + return getSupportedFunctions().contains(functionName.uppercase()) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/specifications/CalculatorValiditySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/specifications/CalculatorValiditySpec.kt new file mode 100644 index 00000000..17522f79 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/specifications/CalculatorValiditySpec.kt @@ -0,0 +1,429 @@ +package hs.kr.entrydsm.domain.evaluator.specifications + +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import hs.kr.entrydsm.domain.calculator.values.MultiStepCalculationRequest +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import hs.kr.entrydsm.global.constants.ErrorCodes +import hs.kr.entrydsm.domain.lexer.aggregates.LexerAggregate + +/** + * POC ์ฝ”๋“œ์˜ FormulaValidator ๊ธฐ๋Šฅ์„ DDD Specification ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ ์š”์ฒญ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ณตํ•ฉ์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ validateCalculationRequest, validateVariables ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ + * ์ฒด๊ณ„์ ์ด๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ช…์„ธ ํŒจํ„ด์œผ๋กœ ์žฌ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.28 + */ +@Specification( + name = "CalculatorValidity", + description = "POC ์ฝ”๋“œ ๊ธฐ๋ฐ˜์˜ ๊ณ„์‚ฐ๊ธฐ ์š”์ฒญ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ช…์„ธ", + domain = "evaluator", + priority = Priority.HIGH +) +class CalculatorValiditySpec { + + companion object { + // POC ์ฝ”๋“œ์˜ CalculatorProperties ๊ธฐ๋ณธ๊ฐ’๋“ค + private const val DEFAULT_MAX_FORMULA_LENGTH = 5000 + private const val DEFAULT_MAX_STEPS = 50 + private const val DEFAULT_MAX_VARIABLES = 100 + + // POC ์ฝ”๋“œ์—์„œ ์‚ฌ์šฉ๋œ ์ •๊ทœ์‹ ํŒจํ„ด๋“ค + private val VALID_VARIABLE_NAME_PATTERN = Regex("^[a-zA-Z_][a-zA-Z0-9_]*$") + private val SUSPICIOUS_PATTERN = Regex("(eval|exec|system|runtime|process)") + + // POC ์ฝ”๋“œ์˜ ํ—ˆ์šฉ๋œ ํ† ํฐ๋“ค + private val ALLOWED_OPERATORS = setOf( + "+", "-", "*", "/", "%", "^", + "==", "!=", "<", "<=", ">", ">=", + "&&", "||", "!", "(", ")" + ) + + private val ALLOWED_FUNCTIONS = setOf( + "ABS", "SQRT", "ROUND", "MIN", "MAX", "SUM", "AVG", "AVERAGE", + "IF", "POW", "LOG", "LOG10", "EXP", "SIN", "COS", "TAN", + "ASIN", "ACOS", "ATAN", "ATAN2", "SINH", "COSH", "TANH", + "ASINH", "ACOSH", "ATANH", "FLOOR", "CEIL", "CEILING", + "TRUNCATE", "TRUNC", "SIGN", "RANDOM", "RAND", "RADIANS", + "DEGREES", "PI", "E", "MOD", "GCD", "LCM", "FACTORIAL", + "COMBINATION", "COMB", "PERMUTATION", "PERM" + ) + + // POC ์ฝ”๋“œ์˜ ์˜ˆ์•ฝ์–ด๋“ค + private val RESERVED_WORDS = setOf( + "null", "undefined", "true", "false", "NaN", "Infinity", + "eval", "function", "var", "let", "const", "class", "interface" + ) + } + + /** + * POC ์ฝ”๋“œ์˜ validateCalculationRequest ๊ตฌํ˜„ + */ + fun isSatisfiedBy( + request: CalculationRequest, + maxFormulaLength: Int = DEFAULT_MAX_FORMULA_LENGTH, + maxVariables: Int = DEFAULT_MAX_VARIABLES + ): Boolean { + return try { + validateFormula(request.formula, maxFormulaLength) && + validateVariables(request.variables, maxVariables) && + validateSecurity(request.formula) && + validateSyntax(request.formula) + } catch (e: Exception) { + false + } + } + + /** + * POC ์ฝ”๋“œ์˜ ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ์š”์ฒญ ๊ฒ€์ฆ + */ + fun isSatisfiedBy( + request: MultiStepCalculationRequest, + maxSteps: Int = DEFAULT_MAX_STEPS, + maxVariables: Int = DEFAULT_MAX_VARIABLES + ): Boolean { + return try { + validateStepCount(request.steps.size, maxSteps) && + validateInitialVariables(request.variables, maxVariables) && + validateStepSequence(request.steps) && + validateStepSecurity(request.steps) + } catch (e: Exception) { + false + } + } + + /** + * POC ์ฝ”๋“œ์˜ ์ˆ˜์‹ ๊ธธ์ด ๋ฐ ๋ณต์žก๋„ ๊ฒ€์ฆ + */ + private fun validateFormula(formula: String, maxLength: Int): Boolean { + return formula.isNotBlank() && + formula.length <= maxLength && + !containsControlCharacters(formula) && + hasBalancedParentheses(formula) + } + + /** + * POC ์ฝ”๋“œ์˜ ๋ณ€์ˆ˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + */ + private fun validateVariables(variables: Map, maxVariables: Int): Boolean { + if (variables.size > maxVariables) { + return false + } + + return variables.keys.all { variableName -> + isValidVariableName(variableName) && + !isReservedWord(variableName) + } && variables.values.all { value -> + isValidVariableValue(value) + } + } + + /** + * POC ์ฝ”๋“œ์˜ ๋ณด์•ˆ ๊ฒ€์ฆ (์˜์‹ฌ์Šค๋Ÿฌ์šด ํŒจํ„ด ๊ฐ์ง€) + */ + private fun validateSecurity(formula: String): Boolean { + val lowerFormula = formula.lowercase() + return !SUSPICIOUS_PATTERN.containsMatchIn(lowerFormula) && + !containsDangerousSequences(lowerFormula) && + !hasExcessiveRecursion(formula) + } + + /** + * POC ์ฝ”๋“œ์˜ ๊ตฌ๋ฌธ ๊ฒ€์ฆ (ํ—ˆ์šฉ๋œ ํ† ํฐ๋งŒ ์‚ฌ์šฉ) + */ + private fun validateSyntax(formula: String): Boolean { + return try { + val tokens = tokenizeFormula(formula) + tokens.all { token -> + isAllowedToken(token) + } + } catch (e: Exception) { + false + } + } + + /** + * ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ์˜ ๋‹จ๊ณ„ ์ˆ˜ ๊ฒ€์ฆ + */ + private fun validateStepCount(stepCount: Int, maxSteps: Int): Boolean { + return stepCount > 0 && stepCount <= maxSteps + } + + /** + * ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ์˜ ์ดˆ๊ธฐ ๋ณ€์ˆ˜ ๊ฒ€์ฆ + */ + private fun validateInitialVariables(variables: Map, maxVariables: Int): Boolean { + return validateVariables(variables, maxVariables) + } + + /** + * ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ์˜ ๋‹จ๊ณ„ ์ˆœ์„œ ๊ฒ€์ฆ + */ + private fun validateStepSequence(steps: List): Boolean { + if (steps.isEmpty()) return false + + // ๋‹จ๊ณ„ ๊ฐ„ ์˜์กด์„ฑ ๊ฒ€์ฆ + val definedVariables = mutableSetOf() + + steps.forEachIndexed { index, step -> + // ๊ฐ ๋‹จ๊ณ„์˜ ์ˆ˜์‹ ๊ฒ€์ฆ + val formula = extractFormulaFromStep(step) + if (!validateFormula(formula, DEFAULT_MAX_FORMULA_LENGTH)) { + return false + } + + // ๋ณ€์ˆ˜ ์˜์กด์„ฑ ๊ฒ€์ฆ + val usedVariables = extractVariablesFromFormula(formula) + val undefinedVariables = usedVariables - definedVariables + + if (undefinedVariables.isNotEmpty() && index > 0) { + // ์ฒซ ๋ฒˆ์งธ ๋‹จ๊ณ„๊ฐ€ ์•„๋‹Œ๋ฐ ์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๊ฐ€ ์žˆ์œผ๋ฉด ์‹คํŒจ + return false + } + + // ์ด ๋‹จ๊ณ„์—์„œ ์ •์˜๋˜๋Š” ๋ณ€์ˆ˜ ์ถ”๊ฐ€ + val assignedVariable = extractAssignedVariable(step) + if (assignedVariable != null) { + definedVariables.add(assignedVariable) + } + } + + return true + } + + /** + * ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ์˜ ๋ณด์•ˆ ๊ฒ€์ฆ + */ + private fun validateStepSecurity(steps: List): Boolean { + return steps.all { step -> + val formula = extractFormulaFromStep(step) + validateSecurity(formula) + } + } + + // Helper methods + + private fun containsControlCharacters(formula: String): Boolean { + return formula.any { char -> + char.isISOControl() && char != '\t' && char != '\n' && char != '\r' + } + } + + private fun hasBalancedParentheses(formula: String): Boolean { + var balance = 0 + for (char in formula) { + when (char) { + '(' -> balance++ + ')' -> balance-- + } + if (balance < 0) return false + } + return balance == 0 + } + + private fun isValidVariableName(name: String): Boolean { + return name.isNotBlank() && + name.length <= 50 && + VALID_VARIABLE_NAME_PATTERN.matches(name) + } + + private fun isReservedWord(name: String): Boolean { + return RESERVED_WORDS.contains(name.lowercase()) + } + + private fun isValidVariableValue(value: Any?): Boolean { + return when (value) { + null -> false + is Number -> { + when (value) { + is Double -> value.isFinite() + is Float -> value.isFinite() + else -> true + } + } + is Boolean -> true + is String -> value.length <= 1000 + else -> false + } + } + + private fun containsDangerousSequences(formula: String): Boolean { + val dangerousSequences = listOf( + "javascript:", "data:", "vbscript:", + "../", "..\\", "./", ".\\", + "", "", + "onload=", "onerror=", "onclick=" + ) + + return dangerousSequences.any { sequence -> + formula.contains(sequence, ignoreCase = true) + } + } + + private fun hasExcessiveRecursion(formula: String): Boolean { + // ๊ฐ„๋‹จํ•œ ์žฌ๊ท€ ํŒจํ„ด ๊ฐ์ง€ + val functionCalls = Regex("[a-zA-Z_][a-zA-Z0-9_]*\\s*\\(").findAll(formula).count() + return functionCalls > 20 + } + + private fun tokenizeFormula(formula: String): List { + val lexer = LexerAggregate() + val result = lexer.tokenize(formula) + + return if (result.isSuccess) { + result.tokens.map { it.value } + } else { + throw EvaluatorException.evaluationError( + cause = RuntimeException("Tokenization failed: ${result.error?.message}") + ) + } + } + + private fun isAllowedToken(token: String): Boolean { + return when { + token.matches(Regex("\\d+(\\.\\d+)?")) -> true // ์ˆซ์ž + VALID_VARIABLE_NAME_PATTERN.matches(token) -> { + // ๋ณ€์ˆ˜๋ช… ๋˜๋Š” ํ•จ์ˆ˜๋ช… + token.uppercase() in ALLOWED_FUNCTIONS || isValidVariableName(token) + } + token in ALLOWED_OPERATORS -> true + else -> false + } + } + + private fun extractFormulaFromStep(step: Any): String { + return when (step) { + is hs.kr.entrydsm.domain.calculator.values.CalculationStep -> step.formula + is String -> step + else -> throw EvaluatorException.unsupportedType( + valueType = step::class.simpleName ?: "Unknown", + value = "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋‹จ๊ณ„ ํƒ€์ž…์ž…๋‹ˆ๋‹ค. CalculationStep ๋˜๋Š” String๋งŒ ์ง€์›๋ฉ๋‹ˆ๋‹ค." + ) + } + } + + private fun extractVariablesFromFormula(formula: String): Set { + val tokens = tokenizeFormula(formula) + return tokens.filter { token -> + VALID_VARIABLE_NAME_PATTERN.matches(token) && + token.uppercase() !in ALLOWED_FUNCTIONS + }.toSet() + } + + private fun extractAssignedVariable(step: Any): String? { + return when (step) { + is hs.kr.entrydsm.domain.calculator.values.CalculationStep -> step.resultVariable + else -> null + } + } + + /** + * ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ธํžˆ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getValidationErrors( + request: CalculationRequest, + maxFormulaLength: Int = DEFAULT_MAX_FORMULA_LENGTH, + maxVariables: Int = DEFAULT_MAX_VARIABLES + ): List> { + val errors = mutableListOf>() + + try { + // ์ˆ˜์‹ ๊ฒ€์ฆ ์˜ค๋ฅ˜ + if (!validateFormula(request.formula, maxFormulaLength)) { + errors.add(mapOf( + "errorCode" to ErrorCodes.Calculator.INVALID_FORMULA.code, + "message" to "์ˆ˜์‹์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: ${request.formula}", + "severity" to "ERROR" + )) + } + + // ๋ณ€์ˆ˜ ๊ฒ€์ฆ ์˜ค๋ฅ˜ + if (!validateVariables(request.variables, maxVariables)) { + errors.add(mapOf( + "errorCode" to ErrorCodes.Calculator.TOO_MANY_VARIABLES.code, + "message" to "๋ณ€์ˆ˜ ๊ฐœ์ˆ˜ ๋˜๋Š” ์œ ํšจ์„ฑ ์˜ค๋ฅ˜: ${request.variables.size}๊ฐœ ๋ณ€์ˆ˜", + "severity" to "ERROR" + )) + } + + // ๋ณด์•ˆ ๊ฒ€์ฆ ์˜ค๋ฅ˜ + if (!validateSecurity(request.formula)) { + errors.add(mapOf( + "errorCode" to ErrorCodes.Evaluator.SECURITY_VIOLATION.code, + "message" to "๋ณด์•ˆ ์œ„๋ฐ˜: ์˜์‹ฌ์Šค๋Ÿฌ์šด ํŒจํ„ด์ด ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค", + "severity" to "CRITICAL" + )) + } + + // ๊ตฌ๋ฌธ ๊ฒ€์ฆ ์˜ค๋ฅ˜ + if (!validateSyntax(request.formula)) { + errors.add(mapOf( + "errorCode" to ErrorCodes.Parser.SYNTAX_ERROR.code, + "message" to "๊ตฌ๋ฌธ ์˜ค๋ฅ˜: ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํ† ํฐ์ด ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค", + "severity" to "ERROR" + )) + } + + } catch (e: Exception) { + errors.add(mapOf( + "errorCode" to ErrorCodes.Common.UNKNOWN_ERROR.code, + "message" to "๊ฒ€์ฆ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + "severity" to "CRITICAL" + )) + } + + return errors + } + + /** + * ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class ValidationError( + val code: String, + val message: String, + val severity: Severity = Severity.ERROR + ) { + enum class Severity { + INFO, WARNING, ERROR, CRITICAL + } + } + + /** + * ๋ช…์„ธ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getConfiguration(): Map = mapOf( + "name" to "CalculatorValiditySpec", + "based_on" to "POC_FormulaValidator", + "maxFormulaLength" to DEFAULT_MAX_FORMULA_LENGTH, + "maxSteps" to DEFAULT_MAX_STEPS, + "maxVariables" to DEFAULT_MAX_VARIABLES, + "allowedOperators" to ALLOWED_OPERATORS.size, + "allowedFunctions" to ALLOWED_FUNCTIONS.size, + "reservedWords" to RESERVED_WORDS.size, + "securityValidation" to true, + "syntaxValidation" to true, + "variableValidation" to true, + "multiStepSupport" to true + ) + + /** + * ๋ช…์„ธ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getStatistics(): Map = mapOf( + "specificationName" to "CalculatorValiditySpec", + "implementedFeatures" to listOf( + "formula_validation", "variable_validation", "security_validation", + "syntax_validation", "multi_step_validation", "step_sequence_validation" + ), + "pocCompatibility" to true, + "validationLayers" to 4, + "securityChecks" to 3, + "priority" to Priority.HIGH.name + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/specifications/ExpressionValiditySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/specifications/ExpressionValiditySpec.kt new file mode 100644 index 00000000..3e8227d5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/specifications/ExpressionValiditySpec.kt @@ -0,0 +1,448 @@ +package hs.kr.entrydsm.domain.evaluator.specifications + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode +import hs.kr.entrydsm.domain.ast.entities.BooleanNode +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.entities.IfNode +import hs.kr.entrydsm.domain.ast.entities.NumberNode +import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode +import hs.kr.entrydsm.domain.ast.entities.VariableNode +import hs.kr.entrydsm.domain.evaluator.entities.EvaluationContext +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.exception.ValidationException + +/** + * ํ‘œํ˜„์‹ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ช…์„ธ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Specification ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ‘œํ˜„์‹์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” + * ๋ณตํ•ฉ์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ๊ตฌ๋ฌธ ๊ฒ€์ฆ, ์˜๋ฏธ ๊ฒ€์ฆ, + * ํƒ€์ž… ๊ฒ€์ฆ ๋“ฑ์„ ํ†ตํ•ด ํ‘œํ˜„์‹์˜ ํ‰๊ฐ€ ๊ฐ€๋Šฅ์„ฑ์„ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Specification( + name = "ExpressionValidity", + description = "ํ‘œํ˜„์‹์˜ ๊ตฌ๋ฌธ์ , ์˜๋ฏธ์  ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ", + domain = "evaluator", + priority = hs.kr.entrydsm.global.annotation.specification.type.Priority.HIGH +) +class ExpressionValiditySpec { + + companion object { + private const val MAX_EXPRESSION_DEPTH = 100 + private const val MAX_VARIABLE_NAME_LENGTH = 100 + private const val MAX_FUNCTION_ARGUMENTS = 50 + private const val MAX_STRING_LENGTH = 10000 + + // ์˜ˆ์•ฝ๋œ ํ•จ์ˆ˜ ์ด๋ฆ„๋“ค + private val RESERVED_FUNCTION_NAMES = setOf( + "eval", "exec", "system", "runtime", "process", "file", "io" + ) + + // ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ•จ์ˆ˜ ํŒจํ„ด๋“ค + private val SUSPICIOUS_FUNCTION_PATTERNS = setOf( + "eval", "exec", "system", "runtime", "process", "file", "io", + "script", "command", "shell", "import", "require", "load" + ) + + // ์˜์‹ฌ์Šค๋Ÿฌ์šด ๋ณ€์ˆ˜ ํŒจํ„ด๋“ค + private val SUSPICIOUS_VARIABLE_PATTERNS = setOf( + "system", "runtime", "process", "file", "path", "command", + "exec", "eval", "shell", "script" + ) + + // ์œ ํšจํ•œ ํ•จ์ˆ˜ ์ด๋ฆ„ ํŒจํ„ด + private val VALID_FUNCTION_NAME_PATTERN = Regex("^[a-zA-Z][a-zA-Z0-9_]*$") + + // ์œ ํšจํ•œ ๋ณ€์ˆ˜ ์ด๋ฆ„ ํŒจํ„ด + private val VALID_VARIABLE_NAME_PATTERN = Regex("^[a-zA-Z_][a-zA-Z0-9_]*$") + } + + /** + * ํ‘œํ˜„์‹์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isSatisfiedBy(node: ASTNode, context: EvaluationContext): Boolean { + return try { + validateSyntax(node) && + validateSemantics(node, context) && + validateStructure(node) && + validateSecurity(node) + } catch (e: Exception) { + throw hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException.evaluationFailed( + RuntimeException("ํ‘œํ˜„์‹ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ: ${e.message}", e) + ) + } + } + + /** + * ํ‘œํ˜„์‹์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค (์ปจํ…์ŠคํŠธ ์—†์ด). + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isSatisfiedBy(node: ASTNode): Boolean { + return try { + validateSyntax(node) && + validateStructure(node) && + validateSecurity(node) + } catch (e: Exception) { + throw hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException.evaluationFailed( + RuntimeException("ํ‘œํ˜„์‹ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์‹คํŒจ: ${e.message}", e) + ) + } + } + + /** + * ๊ตฌ๋ฌธ์  ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateSyntax(node: ASTNode): Boolean { + return when (node) { + is NumberNode -> validateNumberNode(node) + is BooleanNode -> validateBooleanNode(node) + is VariableNode -> validateVariableNode(node) + is BinaryOpNode -> validateBinaryOpNode(node) + is UnaryOpNode -> validateUnaryOpNode(node) + is FunctionCallNode -> validateFunctionCallNode(node) + is IfNode -> validateIfNode(node) + else -> { + // ์•Œ ์ˆ˜ ์—†๋Š” ๋…ธ๋“œ ํƒ€์ž… + false + } + } + } + + /** + * ์˜๋ฏธ์  ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateSemantics(node: ASTNode, context: EvaluationContext): Boolean { + return validateVariableBindings(node, context) && + validateFunctionAvailability(node) && + validateTypeConsistency(node, context) + } + + /** + * ๊ตฌ์กฐ์  ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateStructure(node: ASTNode): Boolean { + return validateDepth(node, 0) && + validateComplexity(node) && + validateCircularReferences(node) + } + + /** + * ๋ณด์•ˆ ๊ฒ€์ฆ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ์•ˆ์ „ํ•˜๋ฉด true + */ + fun validateSecurity(node: ASTNode): Boolean { + return !containsReservedFunctions(node) && + !containsSuspiciousPatterns(node) && + validateFunctionSafety(node) + } + + /** + * ํ‘œํ˜„์‹์—์„œ ๋ฐœ๊ฒฌ๋œ ๋ชจ๋“  ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ (์„ ํƒ์ ) + * @return ์˜ค๋ฅ˜ ๋ชฉ๋ก + */ + fun getValidationErrors(node: ASTNode, context: EvaluationContext? = null): List { + val errors = mutableListOf() + + try { + collectSyntaxErrors(node, errors) + if (context != null) { + collectSemanticErrors(node, context, errors) + } + collectStructuralErrors(node, errors) + collectSecurityErrors(node, errors) + } catch (e: Exception) { + errors.add(ValidationError("UNKNOWN_ERROR", "๊ฒ€์ฆ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}")) + } + + return errors + } + + /** + * ํ‘œํ˜„์‹์˜ ๋ณต์žก๋„ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @return ๋ณต์žก๋„ ์ ์ˆ˜ + */ + fun calculateComplexityScore(node: ASTNode): Int { + return when (node) { + is NumberNode, is BooleanNode, is VariableNode -> 1 + is UnaryOpNode -> 2 + calculateComplexityScore(node.operand) + is BinaryOpNode -> 3 + calculateComplexityScore(node.left) + calculateComplexityScore(node.right) + is FunctionCallNode -> 5 + node.args.sumOf { calculateComplexityScore(it) } + is IfNode -> 7 + calculateComplexityScore(node.condition) + + calculateComplexityScore(node.trueValue) + + calculateComplexityScore(node.falseValue) + else -> 10 // ์•Œ ์ˆ˜ ์—†๋Š” ๋…ธ๋“œ๋Š” ๋†’์€ ๋ณต์žก๋„ + } + } + + // Private validation methods + + private fun validateNumberNode(node: NumberNode): Boolean { + return node.value.isFinite() && !node.value.isNaN() + } + + private fun validateBooleanNode(node: BooleanNode): Boolean { + return true // Boolean ๋…ธ๋“œ๋Š” ํ•ญ์ƒ ์œ ํšจ + } + + private fun validateVariableNode(node: VariableNode): Boolean { + return node.name.isNotBlank() && + node.name.length <= MAX_VARIABLE_NAME_LENGTH && + VALID_VARIABLE_NAME_PATTERN.matches(node.name) + } + + private fun validateBinaryOpNode(node: BinaryOpNode): Boolean { + return validateSyntax(node.left) && + validateSyntax(node.right) && + isValidBinaryOperator(node.operator) + } + + private fun validateUnaryOpNode(node: UnaryOpNode): Boolean { + return validateSyntax(node.operand) && + isValidUnaryOperator(node.operator) + } + + private fun validateFunctionCallNode(node: FunctionCallNode): Boolean { + return node.name.isNotBlank() && + VALID_FUNCTION_NAME_PATTERN.matches(node.name) && + node.args.size <= MAX_FUNCTION_ARGUMENTS && + node.args.all { validateSyntax(it) } && + !RESERVED_FUNCTION_NAMES.contains(node.name.lowercase()) + } + + private fun validateIfNode(node: IfNode): Boolean { + return validateSyntax(node.condition) && + validateSyntax(node.trueValue) && + validateSyntax(node.falseValue) + } + + private fun validateVariableBindings(node: ASTNode, context: EvaluationContext): Boolean { + val requiredVariables = node.getVariables() + return requiredVariables.all { context.hasVariable(it) } + } + + private fun validateFunctionAvailability(node: ASTNode): Boolean { + val usedFunctions = extractFunctionNames(node) + return usedFunctions.all { isKnownFunction(it) } + } + + private fun validateTypeConsistency(node: ASTNode, context: EvaluationContext): Boolean { + // ๊ฐ„๋‹จํ•œ ํƒ€์ž… ์ผ๊ด€์„ฑ ๊ฒ€์‚ฌ + return true // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ๋” ์ •๊ตํ•œ ํƒ€์ž… ๊ฒ€์‚ฌ ํ•„์š” + } + + private fun validateDepth(node: ASTNode, currentDepth: Int): Boolean { + if (currentDepth > MAX_EXPRESSION_DEPTH) { + return false + } + return node.getChildren().all { validateDepth(it, currentDepth + 1) } + } + + private fun validateComplexity(node: ASTNode): Boolean { + val complexity = calculateComplexityScore(node) + return complexity < 1000 // ๋ณต์žก๋„ ์ œํ•œ + } + + private fun validateCircularReferences(node: ASTNode): Boolean { + val visited = mutableSetOf() + + fun checkCircular(current: ASTNode): Boolean { + if (current in visited) { + return false // ์ˆœํ™˜ ์ฐธ์กฐ ๋ฐœ๊ฒฌ + } + visited.add(current) + val result = current.getChildren().all { checkCircular(it) } + visited.remove(current) + return result + } + + return checkCircular(node) + } + + private fun containsReservedFunctions(node: ASTNode): Boolean { + val usedFunctions = extractFunctionNames(node) + return usedFunctions.any { RESERVED_FUNCTION_NAMES.contains(it.lowercase()) } + } + + private fun containsSuspiciousPatterns(node: ASTNode): Boolean { + return when (node) { + is FunctionCallNode -> { + // Check function name directly against suspicious patterns + val functionName = node.name.lowercase() + val isSuspiciousFunction = SUSPICIOUS_FUNCTION_PATTERNS.any { pattern -> + functionName == pattern || functionName.contains(pattern) + } + + // Recursively check function arguments + isSuspiciousFunction || node.args.any { containsSuspiciousPatterns(it) } + } + is VariableNode -> { + // Check variable name directly against suspicious patterns + val variableName = node.name.lowercase() + SUSPICIOUS_VARIABLE_PATTERNS.any { pattern -> + variableName == pattern || variableName.contains(pattern) + } + } + is BinaryOpNode -> { + // Check both operands + containsSuspiciousPatterns(node.left) || containsSuspiciousPatterns(node.right) + } + is UnaryOpNode -> { + // Check the operand + containsSuspiciousPatterns(node.operand) + } + is IfNode -> { + // Check all branches of conditional expression + containsSuspiciousPatterns(node.condition) || + containsSuspiciousPatterns(node.trueValue) || + containsSuspiciousPatterns(node.falseValue) + } + is NumberNode, is BooleanNode -> { + // Primitive values are safe + false + } + else -> { + // For other node types, recursively check all children + node.getChildren().any { containsSuspiciousPatterns(it) } + } + } + } + + private fun validateFunctionSafety(node: ASTNode): Boolean { + // ํ•จ์ˆ˜ ์•ˆ์ „์„ฑ ๊ฒ€์ฆ (์˜ˆ: ๋ฌดํ•œ ์žฌ๊ท€ ๊ฐ€๋Šฅ์„ฑ ๋“ฑ) + return true // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ + } + + private fun isValidBinaryOperator(operator: String): Boolean { + return operator in setOf( + "+", "-", "*", "/", "%", "^", + "==", "!=", "<", "<=", ">", ">=", + "&&", "||" + ) + } + + private fun isValidUnaryOperator(operator: String): Boolean { + return operator in setOf("+", "-", "!") + } + + private fun isKnownFunction(functionName: String): Boolean { + // ์‹ค์ œ๋กœ๋Š” ๋“ฑ๋ก๋œ ํ•จ์ˆ˜ ๋ชฉ๋ก๊ณผ ๋น„๊ต + return functionName.uppercase() in setOf( + "ABS", "SQRT", "ROUND", "MIN", "MAX", "SUM", "AVG", "AVERAGE", + "IF", "POW", "LOG", "LOG10", "EXP", "SIN", "COS", "TAN", + "ASIN", "ACOS", "ATAN", "ATAN2", "SINH", "COSH", "TANH", + "ASINH", "ACOSH", "ATANH", "FLOOR", "CEIL", "CEILING", + "TRUNCATE", "TRUNC", "SIGN", "RANDOM", "RAND", "RADIANS", + "DEGREES", "PI", "E", "MOD", "GCD", "LCM", "FACTORIAL", + "COMBINATION", "COMB", "PERMUTATION", "PERM" + ) + } + + private fun extractFunctionNames(node: ASTNode): Set { + return when (node) { + is FunctionCallNode -> { + setOf(node.name) + node.args.flatMap { extractFunctionNames(it) }.toSet() + } + else -> { + node.getChildren().flatMap { extractFunctionNames(it) }.toSet() + } + } + } + + // Error collection methods + + private fun collectSyntaxErrors(node: ASTNode, errors: MutableList) { + if (!validateSyntax(node)) { + errors.add(ValidationError("SYNTAX_ERROR", "๊ตฌ๋ฌธ ์˜ค๋ฅ˜: $node")) + } + node.getChildren().forEach { collectSyntaxErrors(it, errors) } + } + + private fun collectSemanticErrors(node: ASTNode, context: EvaluationContext, errors: MutableList) { + if (!validateSemantics(node, context)) { + errors.add(ValidationError("SEMANTIC_ERROR", "์˜๋ฏธ ์˜ค๋ฅ˜: $node")) + } + node.getChildren().forEach { collectSemanticErrors(it, context, errors) } + } + + private fun collectStructuralErrors(node: ASTNode, errors: MutableList) { + if (!validateStructure(node)) { + errors.add(ValidationError("STRUCTURAL_ERROR", "๊ตฌ์กฐ ์˜ค๋ฅ˜: $node")) + } + node.getChildren().forEach { collectStructuralErrors(it, errors) } + } + + private fun collectSecurityErrors(node: ASTNode, errors: MutableList) { + if (!validateSecurity(node)) { + errors.add(ValidationError("SECURITY_ERROR", "๋ณด์•ˆ ์˜ค๋ฅ˜: $node")) + } + node.getChildren().forEach { collectSecurityErrors(it, errors) } + } + + /** + * ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class ValidationError( + val code: String, + val message: String, + val severity: Severity = Severity.ERROR + ) { + enum class Severity { + WARNING, ERROR, CRITICAL + } + } + + /** + * ๋ช…์„ธ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxExpressionDepth" to MAX_EXPRESSION_DEPTH, + "maxVariableNameLength" to MAX_VARIABLE_NAME_LENGTH, + "maxFunctionArguments" to MAX_FUNCTION_ARGUMENTS, + "maxStringLength" to MAX_STRING_LENGTH, + "reservedFunctionNames" to RESERVED_FUNCTION_NAMES.size, + "validationRules" to listOf("syntax", "semantics", "structure", "security") + ) + + /** + * ๋ช…์„ธ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "specificationName" to "ExpressionValiditySpec", + "supportedNodeTypes" to 7, + "validationLayers" to 4, + "securityChecks" to 3, + "complexityThreshold" to 1000 + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/specifications/TypeCompatibilitySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/specifications/TypeCompatibilitySpec.kt new file mode 100644 index 00000000..4cbbb647 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/specifications/TypeCompatibilitySpec.kt @@ -0,0 +1,605 @@ +package hs.kr.entrydsm.domain.evaluator.specifications + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.ArgumentsNode +import hs.kr.entrydsm.domain.ast.entities.BinaryOpNode +import hs.kr.entrydsm.domain.ast.entities.BooleanNode +import hs.kr.entrydsm.domain.ast.entities.FunctionCallNode +import hs.kr.entrydsm.domain.ast.entities.IfNode +import hs.kr.entrydsm.domain.ast.entities.NumberNode +import hs.kr.entrydsm.domain.ast.entities.UnaryOpNode +import hs.kr.entrydsm.domain.ast.entities.VariableNode +import hs.kr.entrydsm.domain.evaluator.entities.EvaluationContext +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import hs.kr.entrydsm.domain.util.TypeUtils +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode +import kotlin.reflect.KClass + +/** + * ํƒ€์ž… ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ ๋ช…์„ธ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Specification ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ‘œํ˜„์‹ ํ‰๊ฐ€ ๊ณผ์ •์—์„œ + * ํƒ€์ž… ๊ฐ„์˜ ํ˜ธํ™˜์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * ์—ฐ์‚ฐ์ž์™€ ํ•จ์ˆ˜์˜ ํƒ€์ž… ์š”๊ตฌ์‚ฌํ•ญ์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Specification( + name = "TypeCompatibility", + description = "ํ‘œํ˜„์‹ ๋‚ด ํƒ€์ž… ๊ฐ„์˜ ํ˜ธํ™˜์„ฑ๊ณผ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ", + domain = "evaluator", + priority = hs.kr.entrydsm.global.annotation.specification.type.Priority.NORMAL +) +class TypeCompatibilitySpec { + + companion object { + // ์—ฐ์‚ฐ์ž๋ณ„ ํ—ˆ์šฉ ํƒ€์ž… ๋งคํ•‘ + private val OPERATOR_TYPE_REQUIREMENTS = mapOf( + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž - ์ˆซ์ž ํƒ€์ž…๋งŒ + "+" to TypeRequirement.NUMERIC, + "-" to TypeRequirement.NUMERIC, + "*" to TypeRequirement.NUMERIC, + "/" to TypeRequirement.NUMERIC, + "%" to TypeRequirement.NUMERIC, + "^" to TypeRequirement.NUMERIC, + + // ๋น„๊ต ์—ฐ์‚ฐ์ž - ์ˆซ์ž ํƒ€์ž… + "<" to TypeRequirement.NUMERIC, + "<=" to TypeRequirement.NUMERIC, + ">" to TypeRequirement.NUMERIC, + ">=" to TypeRequirement.NUMERIC, + + // ๋™๋“ฑ ๋น„๊ต - ๋ชจ๋“  ํƒ€์ž… + "==" to TypeRequirement.ANY, + "!=" to TypeRequirement.ANY, + + // ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž - ๋ถˆ๋ฆฐ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ + "&&" to TypeRequirement.BOOLEAN_CONVERTIBLE, + "||" to TypeRequirement.BOOLEAN_CONVERTIBLE, + "!" to TypeRequirement.BOOLEAN_CONVERTIBLE + ) + + // ํ•จ์ˆ˜๋ณ„ ํƒ€์ž… ์š”๊ตฌ์‚ฌํ•ญ + private val FUNCTION_TYPE_REQUIREMENTS = mapOf( + "ABS" to listOf(TypeRequirement.NUMERIC), + "SQRT" to listOf(TypeRequirement.NUMERIC), + "ROUND" to listOf(TypeRequirement.NUMERIC), + "MIN" to listOf(TypeRequirement.NUMERIC), + "MAX" to listOf(TypeRequirement.NUMERIC), + "SUM" to listOf(TypeRequirement.NUMERIC), + "AVG" to listOf(TypeRequirement.NUMERIC), + "IF" to listOf(TypeRequirement.BOOLEAN_CONVERTIBLE, TypeRequirement.ANY, TypeRequirement.ANY), + "POW" to listOf(TypeRequirement.NUMERIC, TypeRequirement.NUMERIC), + "LOG" to listOf(TypeRequirement.NUMERIC), + "LOG10" to listOf(TypeRequirement.NUMERIC), + "EXP" to listOf(TypeRequirement.NUMERIC), + "SIN" to listOf(TypeRequirement.NUMERIC), + "COS" to listOf(TypeRequirement.NUMERIC), + "TAN" to listOf(TypeRequirement.NUMERIC) + ) + + // Boolean์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•œ ํƒ€์ž…๋“ค + private val BOOLEAN_CONVERTIBLE_TYPES = setOf( + Boolean::class, Int::class, Long::class, Float::class, Double::class, + String::class, List::class, Map::class + ) + } + + /** + * ํƒ€์ž… ์š”๊ตฌ์‚ฌํ•ญ์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class TypeRequirement { + NUMERIC, // ์ˆซ์ž ํƒ€์ž…๋งŒ + BOOLEAN_CONVERTIBLE, // Boolean์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•œ ํƒ€์ž… + STRING, // ๋ฌธ์ž์—ด ํƒ€์ž… + ANY, // ๋ชจ๋“  ํƒ€์ž… + SAME // ๋™์ผํ•œ ํƒ€์ž… + } + + /** + * ํ‘œํ˜„์‹์˜ ํƒ€์ž… ํ˜ธํ™˜์„ฑ์ด ๋งŒ์กฑ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ + * @return ํƒ€์ž… ํ˜ธํ™˜์„ฑ์ด ๋งŒ์กฑ๋˜๋ฉด true + */ + fun isSatisfiedBy(node: ASTNode, context: EvaluationContext): Boolean { + return try { + validateTypeCompatibility(node, context) + } catch (e: EvaluatorException) { + throw e + } catch (e: DomainException) { + throw e + } catch (e: Exception) { + // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ˆ์™ธ๋Š” ๊ธ€๋กœ๋ฒŒ ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋กœ ๋ž˜ํ•‘ + throw DomainException( + errorCode = ErrorCode.TYPE_COMPATIBILITY_ERROR, + message = "ํƒ€์ž… ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "contextVariables" to context.variables.keys, + "originalError" to e.javaClass.simpleName + ) + ) + } + } + + /** + * ํ‘œํ˜„์‹์˜ ํƒ€์ž… ํ˜ธํ™˜์„ฑ์ด ๋งŒ์กฑ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค (์ปจํ…์ŠคํŠธ ์—†์ด). + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @return ํƒ€์ž… ํ˜ธํ™˜์„ฑ์ด ๋งŒ์กฑ๋˜๋ฉด true + */ + fun isSatisfiedBy(node: ASTNode): Boolean { + return try { + validateTypeCompatibility(node, null) + } catch (e: EvaluatorException) { + // EvaluatorException์€ ์ด๋ฏธ ์ ์ ˆํžˆ ๊ตฌ์กฐํ™”๋œ ์˜ˆ์™ธ์ด๋ฏ€๋กœ ์žฌ๋ฐœ์ƒ + throw e + } catch (e: DomainException) { + // DomainException์€ ์ด๋ฏธ ์ ์ ˆํžˆ ๊ตฌ์กฐํ™”๋œ ์˜ˆ์™ธ์ด๋ฏ€๋กœ ์žฌ๋ฐœ์ƒ + throw e + } catch (e: Exception) { + // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ˆ์™ธ๋Š” ๊ธ€๋กœ๋ฒŒ ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋กœ ๋ž˜ํ•‘ + throw DomainException( + errorCode = ErrorCode.TYPE_COMPATIBILITY_ERROR, + message = "ํƒ€์ž… ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "hasContext" to false, + "originalError" to e.javaClass.simpleName + ) + ) + } + } + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ์ž์˜ ํƒ€์ž… ํ˜ธํ™˜์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž + * @param leftType ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž ํƒ€์ž… + * @param rightType ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž ํƒ€์ž… + * @return ํ˜ธํ™˜๋˜๋ฉด true + */ + fun areOperandsCompatible(operator: String, leftType: KClass<*>, rightType: KClass<*>): Boolean { + val requirement = OPERATOR_TYPE_REQUIREMENTS[operator] ?: return false + + return when (requirement) { + TypeRequirement.NUMERIC -> { + TypeUtils.isNumericType(leftType) && TypeUtils.isNumericType(rightType) + } + TypeRequirement.BOOLEAN_CONVERTIBLE -> { + isBooleanConvertible(leftType) && isBooleanConvertible(rightType) + } + TypeRequirement.STRING -> { + leftType == String::class && rightType == String::class + } + TypeRequirement.ANY -> true + TypeRequirement.SAME -> leftType == rightType + } + } + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์˜ ํƒ€์ž… ํ˜ธํ™˜์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ๋‹จํ•ญ ์—ฐ์‚ฐ์ž + * @param operandType ํ”ผ์—ฐ์‚ฐ์ž ํƒ€์ž… + * @return ํ˜ธํ™˜๋˜๋ฉด true + */ + fun isOperandCompatible(operator: String, operandType: KClass<*>): Boolean { + val requirement = OPERATOR_TYPE_REQUIREMENTS[operator] ?: return false + + return when (requirement) { + TypeRequirement.NUMERIC -> TypeUtils.isNumericType(operandType) + TypeRequirement.BOOLEAN_CONVERTIBLE -> isBooleanConvertible(operandType) + TypeRequirement.STRING -> operandType == String::class + TypeRequirement.ANY -> true + TypeRequirement.SAME -> true + } + } + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ์˜ ์ธ์ˆ˜ ํƒ€์ž…๋“ค์ด ํ˜ธํ™˜๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param functionName ํ•จ์ˆ˜ ์ด๋ฆ„ + * @param argumentTypes ์ธ์ˆ˜ ํƒ€์ž…๋“ค + * @return ํ˜ธํ™˜๋˜๋ฉด true + */ + fun areArgumentsCompatible(functionName: String, argumentTypes: List>): Boolean { + val requirements = FUNCTION_TYPE_REQUIREMENTS[functionName.uppercase()] ?: return false + + // ๊ฐ€๋ณ€ ์ธ์ˆ˜ ํ•จ์ˆ˜ ์ฒ˜๋ฆฌ + if (functionName.uppercase() in setOf("MIN", "MAX", "SUM", "AVG")) { + return argumentTypes.isNotEmpty() && argumentTypes.all { TypeUtils.isNumericType(it) } + } + + // ๊ณ ์ • ์ธ์ˆ˜ ํ•จ์ˆ˜ ์ฒ˜๋ฆฌ + if (argumentTypes.size != requirements.size) { + return false + } + + return argumentTypes.zip(requirements).all { (argType, requirement) -> + satisfiesRequirement(argType, requirement) + } + } + + /** + * ์กฐ๊ฑด๋ฌธ์˜ ํƒ€์ž… ํ˜ธํ™˜์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param conditionType ์กฐ๊ฑด์‹ ํƒ€์ž… + * @param trueType ์ฐธ ๊ฐ’ ํƒ€์ž… + * @param falseType ๊ฑฐ์ง“ ๊ฐ’ ํƒ€์ž… + * @return ํ˜ธํ™˜๋˜๋ฉด true + */ + fun isConditionalCompatible(conditionType: KClass<*>, trueType: KClass<*>, falseType: KClass<*>): Boolean { + return isBooleanConvertible(conditionType) && + areTypesCompatible(trueType, falseType) + } + + /** + * ๋‘ ํƒ€์ž…์ด ํ˜ธํ™˜๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param type1 ์ฒซ ๋ฒˆ์งธ ํƒ€์ž… + * @param type2 ๋‘ ๋ฒˆ์งธ ํƒ€์ž… + * @return ํ˜ธํ™˜๋˜๋ฉด true + */ + fun areTypesCompatible(type1: KClass<*>, type2: KClass<*>): Boolean { + if (type1 == type2) return true + + // ์ˆซ์ž ํƒ€์ž…๋“ค ๊ฐ„์˜ ํ˜ธํ™˜์„ฑ + if (TypeUtils.isNumericType(type1) && TypeUtils.isNumericType(type2)) { + return true + } + + // Boolean๊ณผ ์ˆซ์ž ํƒ€์ž… ๊ฐ„์˜ ํ˜ธํ™˜์„ฑ + if ((type1 == Boolean::class && TypeUtils.isNumericType(type2)) || + (type2 == Boolean::class && TypeUtils.isNumericType(type1))) { + return true + } + + // String๊ณผ ๋‹ค๋ฅธ ํƒ€์ž…์˜ ํ˜ธํ™˜์„ฑ (๋ชจ๋“  ํƒ€์ž…์€ String์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅ) + if (type1 == String::class || type2 == String::class) { + return true + } + + return false + } + + + /** + * ํƒ€์ž…์ด Boolean์œผ๋กœ ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ™•์ธํ•  ํƒ€์ž… + * @return ๋ณ€ํ™˜ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun isBooleanConvertible(type: KClass<*>): Boolean { + return BOOLEAN_CONVERTIBLE_TYPES.contains(type) + } + + /** + * ํ‘œํ˜„์‹์—์„œ ํƒ€์ž… ํ˜ธํ™˜์„ฑ ์˜ค๋ฅ˜๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param node ๊ฒ€์ฆํ•  AST ๋…ธ๋“œ + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ + * @return ๋ฐœ๊ฒฌ๋œ ํƒ€์ž… ์˜ค๋ฅ˜๋“ค + */ + fun findTypeErrors(node: ASTNode, context: EvaluationContext? = null): List { + val errors = mutableListOf() + collectTypeErrors(node, context, errors) + return errors + } + + /** + * ํ‘œํ˜„์‹์˜ ์˜ˆ์ƒ ๊ฒฐ๊ณผ ํƒ€์ž…์„ ์ถ”๋ก ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ + * @return ์˜ˆ์ƒ ๊ฒฐ๊ณผ ํƒ€์ž… + */ + fun inferResultType(node: ASTNode, context: EvaluationContext? = null): KClass<*> { + return when (node) { + is NumberNode -> Double::class + is BooleanNode -> Boolean::class + is VariableNode -> { + context?.getVariable(node.name)?.let { it::class } ?: Any::class + } + is BinaryOpNode -> inferBinaryOpResultType(node.operator, node.left, node.right, context) + is UnaryOpNode -> inferUnaryOpResultType(node.operator, node.operand, context) + is FunctionCallNode -> inferFunctionResultType(node.name) + is IfNode -> { + val trueType = inferResultType(node.trueValue, context) + val falseType = inferResultType(node.falseValue, context) + if (areTypesCompatible(trueType, falseType)) { + getCommonType(trueType, falseType) + } else { + Any::class + } + } + else -> Any::class + } + } + + // Private helper methods + + private fun validateTypeCompatibility(node: ASTNode, context: EvaluationContext?): Boolean { + return when (node) { + is NumberNode, is BooleanNode -> true + is VariableNode -> context?.hasVariable(node.name) ?: true + is BinaryOpNode -> validateBinaryOpTypes(node, context) + is UnaryOpNode -> validateUnaryOpTypes(node, context) + is FunctionCallNode -> validateFunctionCallTypes(node, context) + is IfNode -> validateIfNodeTypes(node, context) + else -> true + } + } + + private fun validateBinaryOpTypes(node: BinaryOpNode, context: EvaluationContext?): Boolean { + val leftType = inferResultType(node.left, context) + val rightType = inferResultType(node.right, context) + + return areOperandsCompatible(node.operator, leftType, rightType) && + validateTypeCompatibility(node.left, context) && + validateTypeCompatibility(node.right, context) + } + + private fun validateUnaryOpTypes(node: UnaryOpNode, context: EvaluationContext?): Boolean { + val operandType = inferResultType(node.operand, context) + + return isOperandCompatible(node.operator, operandType) && + validateTypeCompatibility(node.operand, context) + } + + private fun validateFunctionCallTypes(node: FunctionCallNode, context: EvaluationContext?): Boolean { + val argumentTypes = node.args.map { inferResultType(it, context) } + + return areArgumentsCompatible(node.name, argumentTypes) && + node.args.all { validateTypeCompatibility(it, context) } + } + + private fun validateIfNodeTypes(node: IfNode, context: EvaluationContext?): Boolean { + val conditionType = inferResultType(node.condition, context) + val trueType = inferResultType(node.trueValue, context) + val falseType = inferResultType(node.falseValue, context) + + return isConditionalCompatible(conditionType, trueType, falseType) && + validateTypeCompatibility(node.condition, context) && + validateTypeCompatibility(node.trueValue, context) && + validateTypeCompatibility(node.falseValue, context) + } + + private fun satisfiesRequirement(type: KClass<*>, requirement: TypeRequirement): Boolean { + return when (requirement) { + TypeRequirement.NUMERIC -> TypeUtils.isNumericType(type) + TypeRequirement.BOOLEAN_CONVERTIBLE -> isBooleanConvertible(type) + TypeRequirement.STRING -> type == String::class + TypeRequirement.ANY -> true + TypeRequirement.SAME -> true // context-dependent + } + } + + private fun inferBinaryOpResultType(operator: String, left: ASTNode, right: ASTNode, context: EvaluationContext?): KClass<*> { + return when (operator) { + "+", "-", "*", "/", "%", "^" -> Double::class + "==", "!=", "<", "<=", ">", ">=", "&&", "||" -> Boolean::class + else -> Any::class + } + } + + private fun inferUnaryOpResultType(operator: String, operand: ASTNode, context: EvaluationContext?): KClass<*> { + return when (operator) { + "+", "-" -> Double::class + "!" -> Boolean::class + else -> Any::class + } + } + + private fun inferFunctionResultType(functionName: String): KClass<*> { + return when (functionName.uppercase()) { + "IF" -> Any::class // ์กฐ๊ฑด์— ๋”ฐ๋ผ ๋‹ค๋ฆ„ + "ABS", "SQRT", "ROUND", "MIN", "MAX", "SUM", "AVG", "POW", + "LOG", "LOG10", "EXP", "SIN", "COS", "TAN", "ASIN", "ACOS", + "ATAN", "ATAN2", "SINH", "COSH", "TANH", "ASINH", "ACOSH", + "ATANH", "FLOOR", "CEIL", "TRUNCATE", "SIGN", "RANDOM", + "RADIANS", "DEGREES", "PI", "E", "MOD", "GCD", "LCM", + "FACTORIAL", "COMBINATION", "PERMUTATION" -> Double::class + else -> Any::class + } + } + + private fun getCommonType(type1: KClass<*>, type2: KClass<*>): KClass<*> { + if (type1 == type2) return type1 + if (TypeUtils.isNumericType(type1) && TypeUtils.isNumericType(type2)) return Double::class + return Any::class + } + + private fun collectTypeErrors(node: ASTNode, context: EvaluationContext?, errors: MutableList) { + try { + when (node) { + is BinaryOpNode -> { + val leftType = inferResultType(node.left, context) + val rightType = inferResultType(node.right, context) + if (!areOperandsCompatible(node.operator, leftType, rightType)) { + errors.add(TypeCompatibilityError( + "BINARY_OP_TYPE_MISMATCH", + "์—ฐ์‚ฐ์ž '${node.operator}'์— ๋Œ€ํ•ด ํƒ€์ž… ${leftType.simpleName}๊ณผ ${rightType.simpleName}์€ ํ˜ธํ™˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค", + node + )) + } + collectTypeErrors(node.left, context, errors) + collectTypeErrors(node.right, context, errors) + } + is UnaryOpNode -> { + val operandType = inferResultType(node.operand, context) + if (!isOperandCompatible(node.operator, operandType)) { + errors.add(TypeCompatibilityError( + "UNARY_OP_TYPE_MISMATCH", + "๋‹จํ•ญ ์—ฐ์‚ฐ์ž '${node.operator}'์— ๋Œ€ํ•ด ํƒ€์ž… ${operandType.simpleName}์€ ํ˜ธํ™˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค", + node + )) + } + collectTypeErrors(node.operand, context, errors) + } + is FunctionCallNode -> { + val argumentTypes = node.args.map { inferResultType(it, context) } + if (!areArgumentsCompatible(node.name, argumentTypes)) { + errors.add(TypeCompatibilityError( + "FUNCTION_ARG_TYPE_MISMATCH", + "ํ•จ์ˆ˜ '${node.name}'์˜ ์ธ์ˆ˜ ํƒ€์ž…๋“ค์ด ํ˜ธํ™˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: ${argumentTypes.map { it.simpleName }}", + node + )) + } + node.args.forEach { collectTypeErrors(it, context, errors) } + } + is IfNode -> { + val conditionType = inferResultType(node.condition, context) + if (!isBooleanConvertible(conditionType)) { + errors.add(TypeCompatibilityError( + "CONDITION_TYPE_MISMATCH", + "์กฐ๊ฑด์‹์˜ ํƒ€์ž… ${conditionType.simpleName}์€ Boolean์œผ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค", + node.condition + )) + } + collectTypeErrors(node.condition, context, errors) + collectTypeErrors(node.trueValue, context, errors) + collectTypeErrors(node.falseValue, context, errors) + } + else -> { + node.getChildren().forEach { collectTypeErrors(it, context, errors) } + } + } + } catch (e: EvaluatorException) { + // ํ‰๊ฐ€๊ธฐ ๋„๋ฉ”์ธ ์˜ˆ์™ธ - ๊ธ€๋กœ๋ฒŒ ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋กœ ๋ณ€ํ™˜ + throw DomainException( + errorCode = ErrorCode.TYPE_EVALUATOR_ERROR, + message = "ํƒ€์ž… ์ถ”๋ก  ์ค‘ ํ‰๊ฐ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "phase" to "collectTypeErrors" + ) + ) + } catch (e: IllegalArgumentException) { + // ์ž˜๋ชป๋œ ์ธ์ˆ˜๋‚˜ ํƒ€์ž…์œผ๋กœ ์ธํ•œ ์˜ค๋ฅ˜ + throw DomainException( + errorCode = ErrorCode.TYPE_ARGUMENT_ERROR, + message = "ํƒ€์ž… ์ถ”๋ก  ์ค‘ ์ธ์ˆ˜ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "phase" to "collectTypeErrors" + ) + ) + } catch (e: ClassCastException) { + // ํƒ€์ž… ์บ์ŠคํŒ… ์˜ค๋ฅ˜ + throw DomainException( + errorCode = ErrorCode.TYPE_CAST_ERROR, + message = "ํƒ€์ž… ์ถ”๋ก  ์ค‘ ์บ์ŠคํŒ… ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "phase" to "collectTypeErrors" + ) + ) + } catch (e: NoSuchElementException) { + // ์ปฌ๋ ‰์…˜์ด๋‚˜ ๋งต์—์„œ ์š”์†Œ๋ฅผ ์ฐพ์ง€ ๋ชปํ•œ ๊ฒฝ์šฐ + throw DomainException( + errorCode = ErrorCode.TYPE_LOOKUP_ERROR, + message = "ํƒ€์ž… ์ถ”๋ก  ์ค‘ ์š”์†Œ ์กฐํšŒ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "phase" to "collectTypeErrors" + ) + ) + } catch (e: NullPointerException) { + // null ์ฐธ์กฐ๋กœ ์ธํ•œ ์˜ค๋ฅ˜ + throw DomainException( + errorCode = ErrorCode.TYPE_NULL_REFERENCE_ERROR, + message = "ํƒ€์ž… ์ถ”๋ก  ์ค‘ null ์ฐธ์กฐ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message ?: "null ๊ฐ’ ์ ‘๊ทผ"}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "phase" to "collectTypeErrors" + ) + ) + } catch (e: UnsupportedOperationException) { + // ์ง€์›๋˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ด๋‚˜ ํƒ€์ž… + throw DomainException( + errorCode = ErrorCode.TYPE_UNSUPPORTED_ERROR, + message = "ํƒ€์ž… ์ถ”๋ก  ์ค‘ ์ง€์›๋˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "phase" to "collectTypeErrors" + ) + ) + } catch (e: RuntimeException) { + // ๊ธฐํƒ€ ๋Ÿฐํƒ€์ž„ ์˜ˆ์™ธ๋“ค (ArithmeticException, IllegalStateException ๋“ฑ) + throw DomainException( + errorCode = ErrorCode.TYPE_RUNTIME_ERROR, + message = "ํƒ€์ž… ์ถ”๋ก  ์ค‘ ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.javaClass.simpleName} - ${e.message}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "originalErrorType" to e.javaClass.simpleName, + "phase" to "collectTypeErrors" + ) + ) + } catch (e: Exception) { + // ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์‚ฌ๋œ ์˜ˆ์™ธ๋“ค (์ตœํ›„์˜ ์ˆ˜๋‹จ) + throw DomainException( + errorCode = ErrorCode.TYPE_COMPATIBILITY_ERROR, + message = "ํƒ€์ž… ํ˜ธํ™˜์„ฑ ๊ฒ€์ฆ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e, + context = mapOf( + "nodeType" to (node::class.simpleName ?: "Unknown"), + "errorType" to e.javaClass.simpleName, + "phase" to "collectTypeErrors" + ) + ) + } + } + + /** + * ํƒ€์ž… ํ˜ธํ™˜์„ฑ ์˜ค๋ฅ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class TypeCompatibilityError( + val code: String, + val message: String, + val node: ASTNode + ) + + /** + * ๋ช…์„ธ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "supportedOperators" to OPERATOR_TYPE_REQUIREMENTS.size, + "supportedFunctions" to FUNCTION_TYPE_REQUIREMENTS.size, + "numericTypes" to TypeUtils.NUMERIC_TYPES.size, + "booleanConvertibleTypes" to BOOLEAN_CONVERTIBLE_TYPES.size, + "typeInferenceEnabled" to true, + "strictTypeChecking" to false + ) + + /** + * ๋ช…์„ธ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "specificationName" to "TypeCompatibilitySpec", + "typeRequirements" to TypeRequirement.values().size, + "operatorRules" to OPERATOR_TYPE_REQUIREMENTS.size, + "functionRules" to FUNCTION_TYPE_REQUIREMENTS.size, + "compatibilityChecks" to listOf("binary_ops", "unary_ops", "function_calls", "conditionals") + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/EvaluationResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/EvaluationResult.kt new file mode 100644 index 00000000..64eddddf --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/EvaluationResult.kt @@ -0,0 +1,306 @@ +package hs.kr.entrydsm.domain.evaluator.values + +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import java.time.LocalDateTime + +/** + * ํ‘œํ˜„์‹ ํ‰๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ํ‰๊ฐ€ ๊ฒฐ๊ณผ ๊ฐ’๊ณผ ํ•จ๊ป˜ ํ‰๊ฐ€ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•œ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +data class EvaluationResult private constructor( + val value: Any?, + val type: ResultType, + val isSuccess: Boolean, + val errorMessage: String?, + val evaluationTime: Long, + val variablesUsed: Set, + val functionsUsed: Set, + val evaluatedAt: LocalDateTime = LocalDateTime.now() +) { + + /** + * ์ˆซ์ž ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Double ์ˆซ์ž ๊ฐ’ + * @throws EvaluatorException ๊ฒฐ๊ณผ๊ฐ€ ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ + */ + fun asNumber(): Double { + return when (value) { + is Double -> value + is Int -> value.toDouble() + is Float -> value.toDouble() + is Long -> value.toDouble() + else -> throw EvaluatorException.numberConversionError(value) + } + } + + /** + * ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Boolean ๊ฐ’ + * @throws EvaluatorException ๊ฒฐ๊ณผ๊ฐ€ ๋ถˆ๋ฆฌ์–ธ์œผ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun asBoolean(): Boolean { + return when (value) { + is Boolean -> value + is Double -> value != 0.0 + is Int -> value != 0 + else -> throw EvaluatorException.unsupportedType("Boolean", value) + } + } + + /** + * ๋ฌธ์ž์—ด ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun asString(): String { + return value?.toString() ?: "null" + } + + /** + * ๊ฒฐ๊ณผ๊ฐ€ ์ˆซ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isNumeric(): Boolean = type == ResultType.NUMBER + + /** + * ๊ฒฐ๊ณผ๊ฐ€ ๋ถˆ๋ฆฌ์–ธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isBoolean(): Boolean = type == ResultType.BOOLEAN + + /** + * ๊ฒฐ๊ณผ๊ฐ€ null์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isNull(): Boolean = type == ResultType.NULL + + /** + * ์˜ค๋ฅ˜ ๊ฒฐ๊ณผ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isError(): Boolean = !isSuccess + + /** + * ํ‰๊ฐ€ ์‹œ๊ฐ„์„ milliseconds ๋‹จ์œ„๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getEvaluationTimeMs(): Long = evaluationTime + + /** + * ํ‰๊ฐ€ ์‹œ๊ฐ„์„ nanoseconds ๋‹จ์œ„๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ‰๊ฐ€ ์‹œ๊ฐ„ (nanoseconds), ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ๋ฐœ์ƒ ์‹œ Long.MAX_VALUE + */ + fun getEvaluationTimeNs(): Long { + return try { + if (evaluationTime > Long.MAX_VALUE / 1_000_000) { + Long.MAX_VALUE + } else { + evaluationTime * 1_000_000 + } + } catch (e: ArithmeticException) { + Long.MAX_VALUE + } + } + + /** + * ์‚ฌ์šฉ๋œ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getVariableCount(): Int = variablesUsed.size + + /** + * ์‚ฌ์šฉ๋œ ํ•จ์ˆ˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getFunctionCount(): Int = functionsUsed.size + + /** + * ์„ฑ๋Šฅ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getPerformanceInfo(): PerformanceInfo { + return PerformanceInfo( + evaluationTime = evaluationTime, + variableCount = variablesUsed.size, + functionCount = functionsUsed.size, + complexity = calculateComplexity() + ) + } + + /** + * ๋ณต์žก๋„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun calculateComplexity(): ComplexityLevel { + val totalOperations = variablesUsed.size + functionsUsed.size + return when { + totalOperations <= 5 -> ComplexityLevel.LOW + totalOperations <= 15 -> ComplexityLevel.MEDIUM + totalOperations <= 30 -> ComplexityLevel.HIGH + else -> ComplexityLevel.VERY_HIGH + } + } + + /** + * ๊ฒฐ๊ณผ ํƒ€์ž… ์—ด๊ฑฐํ˜• + */ + enum class ResultType { + NUMBER, + BOOLEAN, + STRING, + NULL + } + + /** + * ๋ณต์žก๋„ ๋ ˆ๋ฒจ ์—ด๊ฑฐํ˜• + */ + enum class ComplexityLevel { + LOW, + MEDIUM, + HIGH, + VERY_HIGH + } + + /** + * ์„ฑ๋Šฅ ์ •๋ณด ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + */ + data class PerformanceInfo( + val evaluationTime: Long, + val variableCount: Int, + val functionCount: Int, + val complexity: ComplexityLevel + ) + + companion object { + /** + * ์„ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun success( + value: Any?, + evaluationTime: Long, + variablesUsed: Set = emptySet(), + functionsUsed: Set = emptySet() + ): EvaluationResult { + val type = determineType(value) + return EvaluationResult( + value = value, + type = type, + isSuccess = true, + errorMessage = null, + evaluationTime = evaluationTime, + variablesUsed = variablesUsed, + functionsUsed = functionsUsed + ) + } + + /** + * ์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun failure( + errorMessage: String, + evaluationTime: Long, + variablesUsed: Set = emptySet(), + functionsUsed: Set = emptySet() + ): EvaluationResult { + return EvaluationResult( + value = null, + type = ResultType.NULL, + isSuccess = false, + errorMessage = errorMessage, + evaluationTime = evaluationTime, + variablesUsed = variablesUsed, + functionsUsed = functionsUsed + ) + } + + /** + * ์ˆซ์ž ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun ofNumber( + value: Double, + evaluationTime: Long, + variablesUsed: Set = emptySet(), + functionsUsed: Set = emptySet() + ): EvaluationResult { + return EvaluationResult( + value = value, + type = ResultType.NUMBER, + isSuccess = true, + errorMessage = null, + evaluationTime = evaluationTime, + variablesUsed = variablesUsed, + functionsUsed = functionsUsed + ) + } + + /** + * ๋ถˆ๋ฆฌ์–ธ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun ofBoolean( + value: Boolean, + evaluationTime: Long, + variablesUsed: Set = emptySet(), + functionsUsed: Set = emptySet() + ): EvaluationResult { + return EvaluationResult( + value = value, + type = ResultType.BOOLEAN, + isSuccess = true, + errorMessage = null, + evaluationTime = evaluationTime, + variablesUsed = variablesUsed, + functionsUsed = functionsUsed + ) + } + + /** + * ๋ฌธ์ž์—ด ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun ofString( + value: String, + evaluationTime: Long, + variablesUsed: Set = emptySet(), + functionsUsed: Set = emptySet() + ): EvaluationResult { + return EvaluationResult( + value = value, + type = ResultType.STRING, + isSuccess = true, + errorMessage = null, + evaluationTime = evaluationTime, + variablesUsed = variablesUsed, + functionsUsed = functionsUsed + ) + } + + /** + * null ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun ofNull( + evaluationTime: Long, + variablesUsed: Set = emptySet(), + functionsUsed: Set = emptySet() + ): EvaluationResult { + return EvaluationResult( + value = null, + type = ResultType.NULL, + isSuccess = true, + errorMessage = null, + evaluationTime = evaluationTime, + variablesUsed = variablesUsed, + functionsUsed = functionsUsed + ) + } + + /** + * ๊ฐ’์˜ ํƒ€์ž…์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun determineType(value: Any?): ResultType { + return when (value) { + null -> ResultType.NULL + is Double, is Float, is Int, is Long -> ResultType.NUMBER + is Boolean -> ResultType.BOOLEAN + is String -> ResultType.STRING + else -> ResultType.STRING + } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/VariableBinding.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/VariableBinding.kt new file mode 100644 index 00000000..30d5c7cf --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/VariableBinding.kt @@ -0,0 +1,267 @@ +package hs.kr.entrydsm.domain.evaluator.values + +import hs.kr.entrydsm.domain.evaluator.exceptions.EvaluatorException +import java.time.LocalDateTime + +/** + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ๋ณ€์ˆ˜๋ช…๊ณผ ๊ฐ’์˜ ๋ฐ”์ธ๋”ฉ ์ •๋ณด๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ด€๋ฆฌํ•˜๋ฉฐ, + * ํƒ€์ž… ๊ฒ€์ฆ๊ณผ ๋ณ€ํ™˜ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +data class VariableBinding private constructor( + val name: String, + val value: Any?, + val type: VariableType, + val isReadonly: Boolean, + val createdAt: LocalDateTime = LocalDateTime.now() +) { + + init { + if (name.isBlank()) throw EvaluatorException.invalidVariableName(name) + if (!isValidVariableName(name)) throw EvaluatorException.invalidVariableName(name) + if (!isValidValue(value, type)) throw EvaluatorException.unsupportedType(type.toString(), value) + } + + /** + * ์ˆซ์ž ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Double ์ˆซ์ž ๊ฐ’ + * @throws EvaluatorException ๊ฐ’์ด ์ˆซ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ + */ + fun asNumber(): Double { + return when (value) { + is Double -> value + is Int -> value.toDouble() + is Float -> value.toDouble() + is Long -> value.toDouble() + else -> throw EvaluatorException.numberConversionError(value) + } + } + + /** + * ๋ถˆ๋ฆฌ์–ธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return Boolean ๊ฐ’ + * @throws EvaluatorException ๊ฐ’์ด ๋ถˆ๋ฆฌ์–ธ์œผ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun asBoolean(): Boolean { + return when (value) { + is Boolean -> value + is Double -> value != 0.0 + is Int -> value != 0 + else -> throw EvaluatorException.unsupportedType("Boolean", value) + } + } + + /** + * ๋ฌธ์ž์—ด ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun asString(): String { + return value?.toString() ?: "null" + } + + /** + * ๊ฐ’์ด ์ˆซ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isNumeric(): Boolean = type == VariableType.NUMBER + + /** + * ๊ฐ’์ด ๋ถˆ๋ฆฌ์–ธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isBoolean(): Boolean = type == VariableType.BOOLEAN + + /** + * ๊ฐ’์ด ๋ฌธ์ž์—ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isString(): Boolean = type == VariableType.STRING + + /** + * ๊ฐ’์ด null์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isNull(): Boolean = type == VariableType.NULL || value == null + + + /** + * ๋ณ€์ˆ˜๋ฅผ ์ƒˆ๋กœ์šด ๊ฐ’์œผ๋กœ ๋ฐ”์ธ๋”ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param newValue ์ƒˆ๋กœ์šด ๊ฐ’ + * @return ์ƒˆ๋กœ์šด VariableBinding + * @throws EvaluatorException ์ฝ๊ธฐ ์ „์šฉ ๋ณ€์ˆ˜๋ฅผ ์ˆ˜์ •ํ•˜๋ ค๋Š” ๊ฒฝ์šฐ + */ + fun withValue(newValue: Any?): VariableBinding { + if (isReadonly) throw EvaluatorException.invalidVariableName("์ฝ๊ธฐ ์ „์šฉ ๋ณ€์ˆ˜๋Š” ์ˆ˜์ •ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $name") + val newType = determineType(newValue) + return VariableBinding( + name = name, + value = newValue, + type = newType, + isReadonly = isReadonly, + createdAt = createdAt + ) + } + + /** + * ์ฝ๊ธฐ ์ „์šฉ ๋ณ€์ˆ˜๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun asReadonly(): VariableBinding { + return if (isReadonly) this else copy(isReadonly = true) + } + + /** + * ๋ณ€์ˆ˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getInfo(): VariableInfo { + return VariableInfo( + name = name, + type = type, + isReadonly = isReadonly, + hasValue = value != null, + createdAt = createdAt + ) + } + + + companion object { + /** + * ์ˆซ์ž ๋ณ€์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun ofNumber(name: String, value: Double, isReadonly: Boolean = false): VariableBinding { + return VariableBinding( + name = name, + value = value, + type = VariableType.NUMBER, + isReadonly = isReadonly + ) + } + + /** + * ๋ถˆ๋ฆฌ์–ธ ๋ณ€์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun ofBoolean(name: String, value: Boolean, isReadonly: Boolean = false): VariableBinding { + return VariableBinding( + name = name, + value = value, + type = VariableType.BOOLEAN, + isReadonly = isReadonly + ) + } + + /** + * ๋ฌธ์ž์—ด ๋ณ€์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun ofString(name: String, value: String, isReadonly: Boolean = false): VariableBinding { + return VariableBinding( + name = name, + value = value, + type = VariableType.STRING, + isReadonly = isReadonly + ) + } + + /** + * null ๋ณ€์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun ofNull(name: String, isReadonly: Boolean = false): VariableBinding { + return VariableBinding( + name = name, + value = null, + type = VariableType.NULL, + isReadonly = isReadonly + ) + } + + /** + * ์ž๋™ ํƒ€์ž… ๊ฒฐ์ •์œผ๋กœ ๋ณ€์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun of(name: String, value: Any?, isReadonly: Boolean = false): VariableBinding { + val type = determineType(value) + return VariableBinding( + name = name, + value = value, + type = type, + isReadonly = isReadonly + ) + } + + /** + * ์ฝ๊ธฐ ์ „์šฉ ๋ณ€์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun readonly(name: String, value: Any?): VariableBinding { + return of(name, value, isReadonly = true) + } + + /** + * ์ƒ์ˆ˜ ๋ณ€์ˆ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun constant(name: String, value: Any?): VariableBinding { + return readonly(name, value) + } + + /** + * ๋ณ€์ˆ˜๋ช…์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidVariableName(name: String): Boolean { + if (name.isEmpty()) return false + if (!name[0].isLetter() && name[0] != '_') return false + return name.drop(1).all { it.isLetterOrDigit() || it == '_' } + } + + /** + * ๊ฐ’์ด ์ง€์ •๋œ ํƒ€์ž…๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidValue(value: Any?, type: VariableType): Boolean { + return when (type) { + VariableType.NUMBER -> value is Double || value is Int || value is Float || value is Long + VariableType.BOOLEAN -> value is Boolean + VariableType.STRING -> value is String + VariableType.NULL -> value == null + } + } + + /** + * ๊ฐ’์˜ ํƒ€์ž…์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun determineType(value: Any?): VariableType { + return when (value) { + null -> VariableType.NULL + is Double, is Float, is Int, is Long -> VariableType.NUMBER + is Boolean -> VariableType.BOOLEAN + is String -> VariableType.STRING + else -> VariableType.STRING + } + } + + /** + * ๊ธฐ๋ณธ ์ˆ˜ํ•™ ์ƒ์ˆ˜๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getMathConstants(): List { + return listOf( + readonly("PI", kotlin.math.PI), + readonly("E", kotlin.math.E), + readonly("TRUE", true), + readonly("FALSE", false), + readonly("NULL", null) + ) + } + + /** + * ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ๋งต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createBindingMap(bindings: List): Map { + return bindings.associateBy { it.name } + } + + /** + * ๊ฐ’ ๋งต์—์„œ ๋ณ€์ˆ˜ ๋ฐ”์ธ๋”ฉ ๋ฆฌ์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun fromValueMap(values: Map): List { + return values.map { (name, value) -> of(name, value) } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/VariableInfo.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/VariableInfo.kt new file mode 100644 index 00000000..0aac5d44 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/VariableInfo.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.domain.evaluator.values + +import java.time.LocalDateTime + +/** + * ๋ณ€์ˆ˜ ์ •๋ณด ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + */ +data class VariableInfo( + val name: String, + val type: VariableType, + val isReadonly: Boolean, + val hasValue: Boolean, + val createdAt: LocalDateTime +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/VariableType.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/VariableType.kt new file mode 100644 index 00000000..231d3eea --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/evaluator/values/VariableType.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.domain.evaluator.values + +/** + * ๋ณ€์ˆ˜ ํƒ€์ž… ์—ด๊ฑฐํ˜• + */ +enum class VariableType { + NUMBER, + BOOLEAN, + STRING, + NULL +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/aggregates/ExpressionFormatter.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/aggregates/ExpressionFormatter.kt new file mode 100644 index 00000000..4c77f0f2 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/aggregates/ExpressionFormatter.kt @@ -0,0 +1,680 @@ +package hs.kr.entrydsm.domain.expresser.aggregates + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.* +import hs.kr.entrydsm.domain.ast.interfaces.ASTVisitor +import hs.kr.entrydsm.domain.expresser.entities.FormattingOptions +import hs.kr.entrydsm.domain.expresser.entities.FormattingStyle +import hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException +import hs.kr.entrydsm.domain.expresser.values.FormattedExpression +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import kotlin.math.ceil +import kotlin.math.floor +import kotlin.math.roundToInt + +/** + * ์ˆ˜์‹๊ณผ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์–‘ํ•œ ํ˜•ํƒœ๋กœ ํฌ๋งทํŒ…ํ•˜๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค. + * + * AST ๋…ธ๋“œ๋ฅผ ์ˆœํšŒํ•˜๋ฉฐ ์ˆ˜์‹์„ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•˜๊ณ , + * ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ๋‹ค์–‘ํ•œ ํ˜•์‹์œผ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. ์ˆ˜ํ•™์  ํ‘œ๊ธฐ๋ฒ•, ์ค‘์œ„ ํ‘œ๊ธฐ๋ฒ•, + * ์ „์œ„ ํ‘œ๊ธฐ๋ฒ•, ํ›„์œ„ ํ‘œ๊ธฐ๋ฒ• ๋“ฑ์„ ์ง€์›ํ•˜๋ฉฐ, ๊ด„ํ˜ธ ์ตœ์ ํ™”์™€ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ + * ์ฒ˜๋ฆฌ๋ฅผ ํ†ตํ•ด ์ •ํ™•ํ•˜๊ณ  ๊ฐ€๋…์„ฑ ๋†’์€ ์ถœ๋ ฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @property options ํฌ๋งทํŒ… ์˜ต์…˜ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Aggregate(context = "expresser") +class ExpressionFormatter( + private val options: FormattingOptions = FormattingOptions.default() +) : ASTVisitor { + + /** + * AST ๋…ธ๋“œ๋ฅผ ํฌ๋งทํŒ…๋œ ํ‘œํ˜„์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param node ํฌ๋งทํŒ…ํ•  AST ๋…ธ๋“œ + * @return ํฌ๋งทํŒ…๋œ ํ‘œํ˜„์‹ + * @throws ExpresserException ํฌ๋งทํŒ… ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun format(node: ASTNode): FormattedExpression { + return try { + val expression = node.accept(this) + FormattedExpression( + expression = expression, + style = options.style, + options = options + ) + } catch (e: ExpresserException) { + throw e + } catch (e: Exception) { + throw ExpresserException.formattingError("ํฌ๋งทํŒ… ์˜ค๋ฅ˜: ${e.message}", e) + } + } + + /** + * ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ํฌ๋งทํŒ…ํ•  ๊ฒฐ๊ณผ + * @return ํฌ๋งทํŒ…๋œ ๊ฒฐ๊ณผ ๋ฌธ์ž์—ด + * @throws ExpresserException ํฌ๋งทํŒ… ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun formatResult(result: Any?): String { + return try { + when (result) { + null -> "null" + is Double -> formatDouble(result) + is Float -> formatDouble(result.toDouble()) + is Int -> formatInteger(result) + is Long -> formatInteger(result.toInt()) + is Boolean -> formatBoolean(result) + is String -> formatString(result) + else -> result.toString() + } + } catch (e: Exception) { + throw ExpresserException.resultFormattingError(result, e) + } + } + + /** + * NumberNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ์ˆซ์ž๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitNumber(node: NumberNode): String { + return formatDouble(node.value) + } + + /** + * BooleanNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ๋ถˆ๋ฆฐ๊ฐ’์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitBoolean(node: BooleanNode): String { + return formatBoolean(node.value) + } + + /** + * VariableNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ๋ณ€์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitVariable(node: VariableNode): String { + return when (options.style) { + FormattingStyle.MATHEMATICAL -> formatMathematicalVariable(node.name) + FormattingStyle.PROGRAMMING -> formatProgrammingVariable(node.name) + FormattingStyle.LATEX -> formatLatexVariable(node.name) + FormattingStyle.COMPACT -> node.name + FormattingStyle.VERBOSE -> formatVerboseVariable(node.name) + } + } + + /** + * BinaryOpNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ์ดํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitBinaryOp(node: BinaryOpNode): String { + val left = node.left.accept(this) as String + val right = node.right.accept(this) as String + + return when (options.style) { + FormattingStyle.MATHEMATICAL -> formatMathematicalBinaryOp(left, node.operator, right, node) + FormattingStyle.PROGRAMMING -> formatProgrammingBinaryOp(left, node.operator, right, node) + FormattingStyle.LATEX -> formatLatexBinaryOp(left, node.operator, right, node) + FormattingStyle.COMPACT -> formatCompactBinaryOp(left, node.operator, right, node) + FormattingStyle.VERBOSE -> formatVerboseBinaryOp(left, node.operator, right, node) + } + } + + /** + * UnaryOpNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ๋‹จํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitUnaryOp(node: UnaryOpNode): String { + val operand = node.operand.accept(this) as String + + return when (options.style) { + FormattingStyle.MATHEMATICAL -> formatMathematicalUnaryOp(node.operator, operand, node) + FormattingStyle.PROGRAMMING -> formatProgrammingUnaryOp(node.operator, operand, node) + FormattingStyle.LATEX -> formatLatexUnaryOp(node.operator, operand, node) + FormattingStyle.COMPACT -> formatCompactUnaryOp(node.operator, operand, node) + FormattingStyle.VERBOSE -> formatVerboseUnaryOp(node.operator, operand, node) + } + } + + /** + * FunctionCallNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitFunctionCall(node: FunctionCallNode): String { + val args = node.args.map { it.accept(this) as String } + + return when (options.style) { + FormattingStyle.MATHEMATICAL -> formatMathematicalFunction(node.name, args) + FormattingStyle.PROGRAMMING -> formatProgrammingFunction(node.name, args) + FormattingStyle.LATEX -> formatLatexFunction(node.name, args) + FormattingStyle.COMPACT -> formatCompactFunction(node.name, args) + FormattingStyle.VERBOSE -> formatVerboseFunction(node.name, args) + } + } + + /** + * IfNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ์กฐ๊ฑด๋ฌธ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitIf(node: IfNode): String { + val condition = node.condition.accept(this) as String + val trueValue = node.trueValue.accept(this) as String + val falseValue = node.falseValue.accept(this) as String + + return when (options.style) { + FormattingStyle.MATHEMATICAL -> formatMathematicalIf(condition, trueValue, falseValue) + FormattingStyle.PROGRAMMING -> formatProgrammingIf(condition, trueValue, falseValue) + FormattingStyle.LATEX -> formatLatexIf(condition, trueValue, falseValue) + FormattingStyle.COMPACT -> formatCompactIf(condition, trueValue, falseValue) + FormattingStyle.VERBOSE -> formatVerboseIf(condition, trueValue, falseValue) + } + } + + /** + * Double ๊ฐ’์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatDouble(value: Double): String { + return when { + value.isNaN() -> "NaN" + value.isInfinite() -> if (value > 0) "โˆž" else "-โˆž" + value == floor(value) && value <= Long.MAX_VALUE -> { + if (options.showIntegerAsDecimal) { + String.format("%.${options.decimalPlaces}f", value) + } else { + value.toLong().toString() + } + } + else -> { + val formatted = String.format("%.${options.decimalPlaces}f", value) + if (options.removeTrailingZeros) { + formatted.trimEnd('0').trimEnd('.') + } else { + formatted + } + } + } + } + + /** + * Integer ๊ฐ’์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatInteger(value: Int): String { + return if (options.showIntegerAsDecimal) { + String.format("%.${options.decimalPlaces}f", value.toDouble()) + } else { + value.toString() + } + } + + /** + * Boolean ๊ฐ’์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatBoolean(value: Boolean): String { + return when (options.style) { + FormattingStyle.MATHEMATICAL -> if (value) "์ฐธ" else "๊ฑฐ์ง“" + FormattingStyle.PROGRAMMING -> value.toString() + FormattingStyle.LATEX -> if (value) "\\text{true}" else "\\text{false}" + FormattingStyle.COMPACT -> if (value) "T" else "F" + FormattingStyle.VERBOSE -> if (value) "TRUE" else "FALSE" + } + } + + /** + * String ๊ฐ’์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatString(value: String): String { + return when (options.style) { + FormattingStyle.PROGRAMMING -> "\"$value\"" + else -> value + } + } + + /** + * ์ˆ˜ํ•™์  ์Šคํƒ€์ผ์˜ ๋ณ€์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatMathematicalVariable(name: String): String { + return MATHEMATICAL_VARIABLE_MAPPINGS[name.lowercase()] ?: name + } + + /** + * ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ์˜ ๋ณ€์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatProgrammingVariable(name: String): String = name + + /** + * LaTeX ์Šคํƒ€์ผ์˜ ๋ณ€์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatLatexVariable(name: String): String { + return LATEX_VARIABLE_MAPPINGS[name.lowercase()] ?: name + } + + /** + * ์ƒ์„ธํ•œ ์Šคํƒ€์ผ์˜ ๋ณ€์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatVerboseVariable(name: String): String = "๋ณ€์ˆ˜($name)" + + /** + * ์ˆ˜ํ•™์  ์Šคํƒ€์ผ์˜ ์ดํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatMathematicalBinaryOp(left: String, operator: String, right: String, node: BinaryOpNode): String { + val op = MATHEMATICAL_OPERATOR_MAPPINGS[operator] ?: operator + + return if (needsParentheses(node)) { + "($left $op $right)" + } else { + "$left $op $right" + } + } + + /** + * ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ์˜ ์ดํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatProgrammingBinaryOp(left: String, operator: String, right: String, node: BinaryOpNode): String { + val spacing = if (options.addSpaces) " " else "" + return if (needsParentheses(node)) { + "($left$spacing$operator$spacing$right)" + } else { + "$left$spacing$operator$spacing$right" + } + } + + /** + * LaTeX ์Šคํƒ€์ผ์˜ ์ดํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatLatexBinaryOp(left: String, operator: String, right: String, node: BinaryOpNode): String { + val op = LATEX_OPERATOR_MAPPINGS[operator] ?: operator + + return if (operator == "^") { + "$left^{$right}" + } else if (needsParentheses(node)) { + "($left $op $right)" + } else { + "$left $op $right" + } + } + + /** + * ๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ์˜ ์ดํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatCompactBinaryOp(left: String, operator: String, right: String, node: BinaryOpNode): String { + return if (needsParentheses(node)) { + "($left$operator$right)" + } else { + "$left$operator$right" + } + } + + /** + * ์ƒ์„ธํ•œ ์Šคํƒ€์ผ์˜ ์ดํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatVerboseBinaryOp(left: String, operator: String, right: String, node: BinaryOpNode): String { + val opName = VERBOSE_OPERATOR_MAPPINGS[operator] ?: operator + return "($left $opName $right)" + } + + /** + * ์ˆ˜ํ•™์  ์Šคํƒ€์ผ์˜ ๋‹จํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatMathematicalUnaryOp(operator: String, operand: String, node: UnaryOpNode): String { + val op = when (operator) { + "!" -> "ยฌ" + else -> operator + } + return "$op$operand" + } + + /** + * ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ์˜ ๋‹จํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatProgrammingUnaryOp(operator: String, operand: String, node: UnaryOpNode): String { + return "$operator$operand" + } + + /** + * LaTeX ์Šคํƒ€์ผ์˜ ๋‹จํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatLatexUnaryOp(operator: String, operand: String, node: UnaryOpNode): String { + val op = when (operator) { + "!" -> "\\neg" + else -> operator + } + return "$op$operand" + } + + /** + * ๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ์˜ ๋‹จํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatCompactUnaryOp(operator: String, operand: String, node: UnaryOpNode): String { + return "$operator$operand" + } + + /** + * ์ƒ์„ธํ•œ ์Šคํƒ€์ผ์˜ ๋‹จํ•ญ ์—ฐ์‚ฐ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatVerboseUnaryOp(operator: String, operand: String, node: UnaryOpNode): String { + val opName = VERBOSE_UNARY_OPERATOR_MAPPINGS[operator] ?: operator + return "($opName $operand)" + } + + /** + * ์ˆ˜ํ•™์  ์Šคํƒ€์ผ์˜ ํ•จ์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatMathematicalFunction(name: String, args: List): String { + val funcName = MATHEMATICAL_FUNCTION_MAPPINGS[name.lowercase()] ?: name + + return when (name.lowercase()) { + "sqrt" -> "โˆš(${args.joinToString(", ")})" + "exp" -> "e^(${args.joinToString(", ")})" + else -> "$funcName(${args.joinToString(", ")})" + } + } + + /** + * ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ์˜ ํ•จ์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatProgrammingFunction(name: String, args: List): String { + val spacing = if (options.addSpaces) " " else "" + val separator = if (options.addSpaces) ", " else "," + return "$name($spacing${args.joinToString(separator)}$spacing)" + } + + /** + * LaTeX ์Šคํƒ€์ผ์˜ ํ•จ์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatLatexFunction(name: String, args: List): String { + val funcName = LATEX_FUNCTION_MAPPINGS[name.lowercase()] ?: "\\text{$name}" + + return when (name.lowercase()) { + "sqrt" -> "\\sqrt{${args.joinToString(", ")}}" + "exp" -> "\\exp(${args.joinToString(", ")})" + else -> "$funcName(${args.joinToString(", ")})" + } + } + + /** + * ๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ์˜ ํ•จ์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatCompactFunction(name: String, args: List): String { + return "$name(${args.joinToString(",")})" + } + + /** + * ์ƒ์„ธํ•œ ์Šคํƒ€์ผ์˜ ํ•จ์ˆ˜๋ฅผ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatVerboseFunction(name: String, args: List): String { + val funcName = VERBOSE_FUNCTION_MAPPINGS[name.lowercase()] ?: name + return "ํ•จ์ˆ˜_${funcName}(${args.joinToString(", ")})" + } + + /** + * ์ˆ˜ํ•™์  ์Šคํƒ€์ผ์˜ ์กฐ๊ฑด๋ฌธ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatMathematicalIf(condition: String, trueValue: String, falseValue: String): String { + return "if $condition then $trueValue else $falseValue" + } + + /** + * ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ์˜ ์กฐ๊ฑด๋ฌธ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatProgrammingIf(condition: String, trueValue: String, falseValue: String): String { + return "($condition ? $trueValue : $falseValue)" + } + + /** + * LaTeX ์Šคํƒ€์ผ์˜ ์กฐ๊ฑด๋ฌธ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatLatexIf(condition: String, trueValue: String, falseValue: String): String { + return "\\begin{cases} $trueValue & \\text{if } $condition \\\\ $falseValue & \\text{otherwise} \\end{cases}" + } + + /** + * ๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ์˜ ์กฐ๊ฑด๋ฌธ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatCompactIf(condition: String, trueValue: String, falseValue: String): String { + return "if($condition,$trueValue,$falseValue)" + } + + /** + * ์ƒ์„ธํ•œ ์Šคํƒ€์ผ์˜ ์กฐ๊ฑด๋ฌธ์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun formatVerboseIf(condition: String, trueValue: String, falseValue: String): String { + return "๋งŒ์•ฝ $condition ์ด๋ฉด $trueValue ์•„๋‹ˆ๋ฉด $falseValue" + } + + /** + * ๋…ธ๋“œ๊ฐ€ ๊ด„ํ˜ธ๊ฐ€ ํ•„์š”ํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun needsParentheses(node: BinaryOpNode): Boolean { + return when (options.style) { + FormattingStyle.COMPACT -> false + FormattingStyle.VERBOSE -> true + else -> { + // ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„์— ๋”ฐ๋ผ ๊ด„ํ˜ธ ํ•„์š”์„ฑ ํŒ๋‹จ + // ๋‹จ์ˆœํ™”: ๊ธฐ๋ณธ์ ์œผ๋กœ ๊ด„ํ˜ธ ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ + false + } + } + } + + /** + * ArgumentsNode๋ฅผ ๋ฐฉ๋ฌธํ•˜์—ฌ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ํฌ๋งทํŒ…ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visitArguments(node: ArgumentsNode): String { + return node.arguments.joinToString(", ") { it.accept(this) } + } + + /** + * ์—ฐ์‚ฐ์ž์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun getOperatorPrecedence(operator: String): Int { + return when (operator) { + "||" -> 1 + "&&" -> 2 + "==", "!=" -> 3 + "<", "<=", ">", ">=" -> 4 + "+", "-" -> 5 + "*", "/", "%" -> 6 + "^" -> 7 + else -> 0 + } + } + + /** + * ์ƒˆ๋กœ์šด ์˜ต์…˜์œผ๋กœ ํฌ๋งทํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun withOptions(newOptions: FormattingOptions): ExpressionFormatter { + return ExpressionFormatter(newOptions) + } + + /** + * ํ˜„์žฌ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getOptions(): FormattingOptions = options + + /** + * ํฌ๋งทํ„ฐ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getFormatterStatistics(): Map = mapOf( + "style" to options.style, + "decimalPlaces" to options.decimalPlaces, + "addSpaces" to options.addSpaces, + "showIntegerAsDecimal" to options.showIntegerAsDecimal, + "removeTrailingZeros" to options.removeTrailingZeros, + "supportedStyles" to FormattingStyle.values().map { it.name } + ) + + companion object { + /** + * ์ˆ˜ํ•™์  ๋ณ€์ˆ˜๋ช… ๋งคํ•‘ + */ + private val MATHEMATICAL_VARIABLE_MAPPINGS = mapOf( + "pi" to "ฯ€", + "e" to "e", + "alpha" to "ฮฑ", + "beta" to "ฮฒ", + "gamma" to "ฮณ", + "delta" to "ฮด", + "epsilon" to "ฮต", + "theta" to "ฮธ", + "lambda" to "ฮป", + "mu" to "ฮผ", + "sigma" to "ฯƒ", + "phi" to "ฯ†", + "omega" to "ฯ‰" + ) + + /** + * LaTeX ๋ณ€์ˆ˜๋ช… ๋งคํ•‘ + */ + private val LATEX_VARIABLE_MAPPINGS = mapOf( + "pi" to "\\pi", + "e" to "e", + "alpha" to "\\alpha", + "beta" to "\\beta", + "gamma" to "\\gamma", + "delta" to "\\delta", + "epsilon" to "\\epsilon", + "theta" to "\\theta", + "lambda" to "\\lambda", + "mu" to "\\mu", + "sigma" to "\\sigma", + "phi" to "\\phi", + "omega" to "\\omega" + ) + + /** + * ์ˆ˜ํ•™์  ์Šคํƒ€์ผ ์—ฐ์‚ฐ์ž ๋งคํ•‘ + */ + private val MATHEMATICAL_OPERATOR_MAPPINGS = mapOf( + "*" to "ร—", + "/" to "รท", + "==" to "=", + "!=" to "โ‰ ", + "<=" to "โ‰ค", + ">=" to "โ‰ฅ", + "&&" to "โˆง", + "||" to "โˆจ", + "^" to "^" + ) + + /** + * LaTeX ์—ฐ์‚ฐ์ž ๋งคํ•‘ + */ + private val LATEX_OPERATOR_MAPPINGS = mapOf( + "*" to "\\times", + "/" to "\\div", + "==" to "=", + "!=" to "\\neq", + "<=" to "\\leq", + ">=" to "\\geq", + "&&" to "\\land", + "||" to "\\lor", + "^" to "^" + ) + + /** + * ์ˆ˜ํ•™์  ์Šคํƒ€์ผ ํ•จ์ˆ˜๋ช… ๋งคํ•‘ + */ + private val MATHEMATICAL_FUNCTION_MAPPINGS = mapOf( + "sin" to "sin", + "cos" to "cos", + "tan" to "tan", + "sqrt" to "โˆš", + "log" to "ln", + "exp" to "e^" + ) + + /** + * LaTeX ํ•จ์ˆ˜๋ช… ๋งคํ•‘ + */ + private val LATEX_FUNCTION_MAPPINGS = mapOf( + "sin" to "\\sin", + "cos" to "\\cos", + "tan" to "tan", + "sqrt" to "\\sqrt", + "log" to "\\ln", + "exp" to "\\exp" + ) + + /** + * ์ƒ์„ธ ์Šคํƒ€์ผ ์—ฐ์‚ฐ์ž๋ช… ๋งคํ•‘ + */ + private val VERBOSE_OPERATOR_MAPPINGS = mapOf( + "+" to "๋”ํ•˜๊ธฐ", + "-" to "๋นผ๊ธฐ", + "*" to "๊ณฑํ•˜๊ธฐ", + "/" to "๋‚˜๋ˆ„๊ธฐ", + "%" to "๋‚˜๋จธ์ง€", + "^" to "๊ฑฐ๋“ญ์ œ๊ณฑ", + "==" to "๊ฐ™๋‹ค", + "!=" to "๋‹ค๋ฅด๋‹ค", + "<" to "์ž‘๋‹ค", + "<=" to "์ž‘๊ฑฐ๋‚˜ ๊ฐ™๋‹ค", + ">" to "ํฌ๋‹ค", + ">=" to "ํฌ๊ฑฐ๋‚˜ ๊ฐ™๋‹ค", + "&&" to "๊ทธ๋ฆฌ๊ณ ", + "||" to "๋˜๋Š”" + ) + + /** + * ์ƒ์„ธ ์Šคํƒ€์ผ ๋‹จํ•ญ ์—ฐ์‚ฐ์ž๋ช… ๋งคํ•‘ + */ + private val VERBOSE_UNARY_OPERATOR_MAPPINGS = mapOf( + "+" to "์–‘์ˆ˜", + "-" to "์Œ์ˆ˜", + "!" to "NOT" + ) + + /** + * ์ƒ์„ธ ์Šคํƒ€์ผ ํ•จ์ˆ˜๋ช… ๋งคํ•‘ + */ + private val VERBOSE_FUNCTION_MAPPINGS = mapOf( + "sin" to "์‚ฌ์ธ", + "cos" to "์ฝ”์‚ฌ์ธ", + "tan" to "ํƒ„์  ํŠธ", + "sqrt" to "์ œ๊ณฑ๊ทผ", + "log" to "์ž์—ฐ๋กœ๊ทธ", + "exp" to "์ง€์ˆ˜", + "abs" to "์ ˆ๋Œ“๊ฐ’", + "floor" to "๋‚ด๋ฆผ", + "ceil" to "์˜ฌ๋ฆผ", + "round" to "๋ฐ˜์˜ฌ๋ฆผ", + "min" to "์ตœ์†Ÿ๊ฐ’", + "max" to "์ตœ๋Œ“๊ฐ’", + "pow" to "๊ฑฐ๋“ญ์ œ๊ณฑ" + ) + + /** + * ๊ธฐ๋ณธ ์˜ต์…˜์œผ๋กœ ํฌ๋งทํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createDefault(): ExpressionFormatter = ExpressionFormatter() + + /** + * ์ˆ˜ํ•™์  ์Šคํƒ€์ผ ํฌ๋งทํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createMathematical(): ExpressionFormatter = + ExpressionFormatter(FormattingOptions.mathematical()) + + /** + * ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ ํฌ๋งทํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createProgramming(): ExpressionFormatter = + ExpressionFormatter(FormattingOptions.programming()) + + /** + * LaTeX ์Šคํƒ€์ผ ํฌ๋งทํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createLatex(): ExpressionFormatter = + ExpressionFormatter(FormattingOptions.latex()) + + /** + * ๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ ํฌ๋งทํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createCompact(): ExpressionFormatter = + ExpressionFormatter(FormattingOptions.compact()) + + /** + * ์ƒ์„ธํ•œ ์Šคํƒ€์ผ ํฌ๋งทํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createVerbose(): ExpressionFormatter = + ExpressionFormatter(FormattingOptions.verbose()) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/aggregates/ExpressionReporter.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/aggregates/ExpressionReporter.kt new file mode 100644 index 00000000..fc0192cd --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/aggregates/ExpressionReporter.kt @@ -0,0 +1,529 @@ +package hs.kr.entrydsm.domain.expresser.aggregates + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.calculator.values.CalculationResult +import hs.kr.entrydsm.domain.expresser.entities.FormattingOptions +import hs.kr.entrydsm.domain.expresser.entities.FormattingStyle +import hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException +import hs.kr.entrydsm.domain.expresser.values.FormattedExpression +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import kotlin.math.roundToInt + +/** + * ๊ณ„์‚ฐ ๊ฒฐ๊ณผ์™€ ์ˆ˜์‹์„ ์ข…ํ•ฉ์ ์œผ๋กœ ๋ณด๊ณ ํ•˜๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ ๊ณผ์ •, ๊ฒฐ๊ณผ, ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ํฌ๊ด„์ ์ธ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๋‹ค์–‘ํ•œ ํฌ๋งท(ํ…์ŠคํŠธ, HTML, JSON, XML)์œผ๋กœ ๊ฒฐ๊ณผ๋ฅผ ์ถœ๋ ฅํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, + * ๊ต์œก์šฉ, ๋””๋ฒ„๊น…์šฉ, ๋ฌธ์„œํ™”์šฉ ๋“ฑ ๋‹ค์–‘ํ•œ ๋ชฉ์ ์— ๋งž๋Š” ๋ณด๊ณ ์„œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @property formatter ์ˆ˜์‹ ํฌ๋งทํ„ฐ + * @property includeSteps ๋‹จ๊ณ„๋ณ„ ์„ค๋ช… ํฌํ•จ ์—ฌ๋ถ€ + * @property includeStatistics ํ†ต๊ณ„ ์ •๋ณด ํฌํ•จ ์—ฌ๋ถ€ + * @property includeAST AST ์ •๋ณด ํฌํ•จ ์—ฌ๋ถ€ + * @property maxDepth ์ตœ๋Œ€ ๋ณด๊ณ  ๊นŠ์ด + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Aggregate(context = "expresser") +class ExpressionReporter( + private val formatter: ExpressionFormatter = ExpressionFormatter.createDefault(), + private val includeSteps: Boolean = true, + private val includeStatistics: Boolean = true, + private val includeAST: Boolean = false, + private val maxDepth: Int = 10 +) { + + /** + * ๋‹จ์ผ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + * @return ๋ณด๊ณ ์„œ ๋ฌธ์ž์—ด + * @throws ExpresserException ๋ณด๊ณ ์„œ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun generateReport(result: CalculationResult): String { + return try { + buildString { + appendLine("=== ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ๋ณด๊ณ ์„œ ===") + appendLine() + + // ๊ธฐ๋ณธ ์ •๋ณด + appendLine("๐Ÿ“Š ๊ธฐ๋ณธ ์ •๋ณด") + appendLine("์ˆ˜์‹: ${result.formula}") + appendLine("๊ฒฐ๊ณผ: ${formatter.formatResult(result.result)}") + appendLine("์‹คํ–‰ ์‹œ๊ฐ„: ${result.executionTimeMs}ms") + appendLine() + + // ๋ณ€์ˆ˜ ์ •๋ณด + if (result.variables.isNotEmpty()) { + appendLine("๐Ÿ”ข ๋ณ€์ˆ˜ ์ •๋ณด") + result.variables.forEach { (name, value) -> + appendLine(" $name = ${formatter.formatResult(value)}") + } + appendLine() + } + + // ๋‹จ๊ณ„๋ณ„ ์„ค๋ช… + if (includeSteps && result.steps.isNotEmpty()) { + appendLine("๐Ÿ“‹ ์‹คํ–‰ ๋‹จ๊ณ„") + result.steps.forEachIndexed { index, step -> + appendLine(" ${index + 1}. $step") + } + appendLine() + } + + // AST ์ •๋ณด + if (includeAST && result.ast != null) { + appendLine("๐ŸŒณ AST ๊ตฌ์กฐ") + val formattedAST = formatter.format(result.ast) + appendLine(" ํฌ๋งทํŒ…๋œ ์ˆ˜์‹: ${formattedAST.expression}") + appendLine(" ๋ณต์žก๋„: ${formattedAST.calculateComplexity()}") + appendLine(" ๊ฐ€๋…์„ฑ: ${formattedAST.calculateReadability()}") + appendLine() + } + + // ํ†ต๊ณ„ ์ •๋ณด + if (includeStatistics) { + appendLine("๐Ÿ“ˆ ํ†ต๊ณ„ ์ •๋ณด") + appendLine(" ์ˆ˜์‹ ๊ธธ์ด: ${result.formula.length}์ž") + appendLine(" ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜: ${result.variables.size}๊ฐœ") + appendLine(" ์‹คํ–‰ ๋‹จ๊ณ„: ${result.steps.size}๋‹จ๊ณ„") + + // ๊ฒฐ๊ณผ ํƒ€์ž… ๋ถ„์„ + val resultType = when (result.result) { + is Double -> "์‹ค์ˆ˜" + is Int -> "์ •์ˆ˜" + is Boolean -> "๋…ผ๋ฆฌ๊ฐ’" + is String -> "๋ฌธ์ž์—ด" + else -> "๊ธฐํƒ€" + } + appendLine(" ๊ฒฐ๊ณผ ํƒ€์ž…: $resultType") + + // ์„ฑ๋Šฅ ๋ถ„์„ + val performance = when { + result.executionTimeMs < 1 -> "๋งค์šฐ ๋น ๋ฆ„" + result.executionTimeMs < 10 -> "๋น ๋ฆ„" + result.executionTimeMs < 100 -> "๋ณดํ†ต" + result.executionTimeMs < 1000 -> "๋А๋ฆผ" + else -> "๋งค์šฐ ๋А๋ฆผ" + } + appendLine(" ์„ฑ๋Šฅ: $performance") + appendLine() + } + + appendLine("๋ณด๊ณ ์„œ ์ƒ์„ฑ ์‹œ๊ฐ„: ${java.time.LocalDateTime.now()}") + } + } catch (e: Exception) { + throw ExpresserException.reportGenerationError(e) + } + } + + /** + * ๋‹ค์ค‘ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param results ๊ณ„์‚ฐ ๊ฒฐ๊ณผ๋“ค + * @return ๋ณด๊ณ ์„œ ๋ฌธ์ž์—ด + * @throws ExpresserException ๋ณด๊ณ ์„œ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun generateMultiStepReport(results: List): String { + return try { + buildString { + appendLine("=== ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ๋ณด๊ณ ์„œ ===") + appendLine() + + // ์š”์•ฝ ์ •๋ณด + appendLine("๐Ÿ“Š ์š”์•ฝ ์ •๋ณด") + appendLine("์ด ๋‹จ๊ณ„ ์ˆ˜: ${results.size}") + appendLine("์ด ์‹คํ–‰ ์‹œ๊ฐ„: ${results.sumOf { it.executionTimeMs }}ms") + appendLine("ํ‰๊ท  ์‹คํ–‰ ์‹œ๊ฐ„: ${results.map { it.executionTimeMs }.average().roundToInt()}ms") + appendLine() + + // ๊ฐ ๋‹จ๊ณ„๋ณ„ ๊ฒฐ๊ณผ + results.forEachIndexed { index, result -> + appendLine("=== ๋‹จ๊ณ„ ${index + 1} ===") + appendLine("์ˆ˜์‹: ${result.formula}") + appendLine("๊ฒฐ๊ณผ: ${formatter.formatResult(result.result)}") + appendLine("์‹คํ–‰ ์‹œ๊ฐ„: ${result.executionTimeMs}ms") + + if (result.variables.isNotEmpty()) { + appendLine("๋ณ€์ˆ˜:") + result.variables.forEach { (name, value) -> + appendLine(" $name = ${formatter.formatResult(value)}") + } + } + appendLine() + } + + // ์ „์ฒด ํ†ต๊ณ„ + if (includeStatistics) { + appendLine("๐Ÿ“ˆ ์ „์ฒด ํ†ต๊ณ„") + val totalFormulaLength = results.sumOf { it.formula.length } + val totalVariables = results.sumOf { it.variables.size } + val successRate = results.count { it.result != null } * 100.0 / results.size + + appendLine(" ์ด ์ˆ˜์‹ ๊ธธ์ด: ${totalFormulaLength}์ž") + appendLine(" ์ด ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜: ${totalVariables}๊ฐœ") + appendLine(" ์„ฑ๊ณต๋ฅ : ${String.format("%.1f", successRate)}%") + + // ์„ฑ๋Šฅ ๋ถ„์„ + val minTime = results.minOfOrNull { it.executionTimeMs } ?: 0 + val maxTime = results.maxOfOrNull { it.executionTimeMs } ?: 0 + appendLine(" ์ตœ์†Œ ์‹คํ–‰ ์‹œ๊ฐ„: ${minTime}ms") + appendLine(" ์ตœ๋Œ€ ์‹คํ–‰ ์‹œ๊ฐ„: ${maxTime}ms") + appendLine() + } + + appendLine("๋ณด๊ณ ์„œ ์ƒ์„ฑ ์‹œ๊ฐ„: ${java.time.LocalDateTime.now()}") + } + } catch (e: Exception) { + throw ExpresserException.reportGenerationError(e) + } + } + + /** + * HTML ํ˜•ํƒœ์˜ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + * @return HTML ๋ณด๊ณ ์„œ ๋ฌธ์ž์—ด + * @throws ExpresserException ๋ณด๊ณ ์„œ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun generateHtmlReport(result: CalculationResult): String { + return try { + buildString { + appendLine("") + appendLine("") + appendLine("") + appendLine(" ") + appendLine(" ") + appendLine(" ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ๋ณด๊ณ ์„œ") + appendLine(" ") + appendLine("") + appendLine("") + appendLine("
") + appendLine("

๐Ÿ“Š ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ๋ณด๊ณ ์„œ

") + appendLine("
") + + appendLine("
") + appendLine("

๊ธฐ๋ณธ ์ •๋ณด

") + appendLine("

์ˆ˜์‹: ${escapeHtml(result.formula)}

") + appendLine("

๊ฒฐ๊ณผ: ${escapeHtml(formatter.formatResult(result.result))}

") + appendLine("

์‹คํ–‰ ์‹œ๊ฐ„: ${result.executionTimeMs}ms

") + appendLine("
") + + if (result.variables.isNotEmpty()) { + appendLine("
") + appendLine("

๐Ÿ”ข ๋ณ€์ˆ˜ ์ •๋ณด

") + result.variables.forEach { (name, value) -> + appendLine("
") + appendLine(" $name = ${escapeHtml(formatter.formatResult(value))}") + appendLine("
") + } + appendLine("
") + } + + if (includeSteps && result.steps.isNotEmpty()) { + appendLine("
") + appendLine("

๐Ÿ“‹ ์‹คํ–‰ ๋‹จ๊ณ„

") + result.steps.forEachIndexed { index, step -> + appendLine("
") + appendLine(" ${index + 1}. ${escapeHtml(step)}") + appendLine("
") + } + appendLine("
") + } + + if (includeStatistics) { + appendLine("
") + appendLine("

๐Ÿ“ˆ ํ†ต๊ณ„ ์ •๋ณด

") + appendLine("
") + appendLine("

์ˆ˜์‹ ๊ธธ์ด: ${result.formula.length}์ž

") + appendLine("

๋ณ€์ˆ˜ ๊ฐœ์ˆ˜: ${result.variables.size}๊ฐœ

") + appendLine("

์‹คํ–‰ ๋‹จ๊ณ„: ${result.steps.size}๋‹จ๊ณ„

") + appendLine("
") + appendLine("
") + } + + appendLine("
") + appendLine("

๋ณด๊ณ ์„œ ์ƒ์„ฑ ์‹œ๊ฐ„: ${java.time.LocalDateTime.now()}

") + appendLine("
") + appendLine("") + appendLine("") + } + } catch (e: Exception) { + throw ExpresserException.reportGenerationError(e) + } + } + + /** + * JSON ํ˜•ํƒœ์˜ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + * @return JSON ๋ณด๊ณ ์„œ ๋ฌธ์ž์—ด + * @throws ExpresserException ๋ณด๊ณ ์„œ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun generateJsonReport(result: CalculationResult): String { + return try { + buildString { + appendLine("{") + appendLine(" \"report\": {") + appendLine(" \"title\": \"๊ณ„์‚ฐ ๊ฒฐ๊ณผ ๋ณด๊ณ ์„œ\",") + appendLine(" \"generatedAt\": \"${java.time.LocalDateTime.now()}\",") + appendLine(" \"basicInfo\": {") + appendLine(" \"formula\": \"${escapeJson(result.formula)}\",") + appendLine(" \"result\": \"${escapeJson(formatter.formatResult(result.result))}\",") + appendLine(" \"executionTimeMs\": ${result.executionTimeMs}") + appendLine(" },") + + if (result.variables.isNotEmpty()) { + appendLine(" \"variables\": {") + result.variables.entries.forEachIndexed { index, (name, value) -> + val comma = if (index < result.variables.size - 1) "," else "" + appendLine(" \"$name\": \"${escapeJson(formatter.formatResult(value))}\"$comma") + } + appendLine(" },") + } + + if (includeSteps && result.steps.isNotEmpty()) { + appendLine(" \"steps\": [") + result.steps.forEachIndexed { index, step -> + val comma = if (index < result.steps.size - 1) "," else "" + appendLine(" \"${escapeJson(step)}\"$comma") + } + appendLine(" ],") + } + + if (includeStatistics) { + appendLine(" \"statistics\": {") + appendLine(" \"formulaLength\": ${result.formula.length},") + appendLine(" \"variableCount\": ${result.variables.size},") + appendLine(" \"stepCount\": ${result.steps.size}") + appendLine(" }") + } else { + // ๋งˆ์ง€๋ง‰ ์‰ผํ‘œ ์ œ๊ฑฐ + val content = this.toString() + this.clear() + append(content.trimEnd().removeSuffix(",")) + } + + appendLine(" }") + appendLine("}") + } + } catch (e: Exception) { + throw ExpresserException.reportGenerationError(e) + } + } + + /** + * XML ํ˜•ํƒœ์˜ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ๊ณ„์‚ฐ ๊ฒฐ๊ณผ + * @return XML ๋ณด๊ณ ์„œ ๋ฌธ์ž์—ด + * @throws ExpresserException ๋ณด๊ณ ์„œ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun generateXmlReport(result: CalculationResult): String { + return try { + buildString { + appendLine("") + appendLine("") + appendLine(" ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ๋ณด๊ณ ์„œ") + appendLine(" ${java.time.LocalDateTime.now()}") + + appendLine(" ") + appendLine(" ") + appendLine(" ") + appendLine(" ${result.executionTimeMs}") + appendLine(" ") + + if (result.variables.isNotEmpty()) { + appendLine(" ") + result.variables.forEach { (name, value) -> + appendLine(" ") + appendLine(" $name") + appendLine(" ") + appendLine(" ") + } + appendLine(" ") + } + + if (includeSteps && result.steps.isNotEmpty()) { + appendLine(" ") + result.steps.forEachIndexed { index, step -> + appendLine(" ") + appendLine(" ") + appendLine(" ") + } + appendLine(" ") + } + + if (includeStatistics) { + appendLine(" ") + appendLine(" ${result.formula.length}") + appendLine(" ${result.variables.size}") + appendLine(" ${result.steps.size}") + appendLine(" ") + } + + appendLine("") + } + } catch (e: Exception) { + throw ExpresserException.reportGenerationError(e) + } + } + + /** + * ๋น„๊ต ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param results ๋น„๊ตํ•  ๊ฒฐ๊ณผ๋“ค + * @return ๋น„๊ต ๋ณด๊ณ ์„œ ๋ฌธ์ž์—ด + * @throws ExpresserException ๋ณด๊ณ ์„œ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun generateComparisonReport(results: List): String { + return try { + buildString { + appendLine("=== ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ๋น„๊ต ๋ณด๊ณ ์„œ ===") + appendLine() + + appendLine("๐Ÿ“Š ๋น„๊ต ๊ฐœ์š”") + appendLine("๋น„๊ต ๋Œ€์ƒ: ${results.size}๊ฐœ") + appendLine("์ƒ์„ฑ ์‹œ๊ฐ„: ${java.time.LocalDateTime.now()}") + appendLine() + + // ๊ฒฐ๊ณผ ๋น„๊ต ํ…Œ์ด๋ธ” + appendLine("๐Ÿ“‹ ๊ฒฐ๊ณผ ๋น„๊ต") + appendLine("โ”Œโ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”") + appendLine("โ”‚ ์ˆœ๋ฒˆ โ”‚ ์ˆ˜์‹ โ”‚ ๊ฒฐ๊ณผ โ”‚ ์‹คํ–‰์‹œ๊ฐ„(ms) โ”‚") + appendLine("โ”œโ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค") + + results.forEachIndexed { index, result -> + val formula = result.formula.take(36).padEnd(36) + val resultStr = formatter.formatResult(result.result).take(28).padEnd(28) + val timeStr = result.executionTimeMs.toString().padStart(11) + appendLine("โ”‚ ${(index + 1).toString().padStart(3)} โ”‚ $formula โ”‚ $resultStr โ”‚ $timeStr โ”‚") + } + + appendLine("โ””โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜") + appendLine() + + // ํ†ต๊ณ„ ๋ถ„์„ + if (includeStatistics) { + appendLine("๐Ÿ“ˆ ํ†ต๊ณ„ ๋ถ„์„") + val executionTimes = results.map { it.executionTimeMs } + val formulaLengths = results.map { it.formula.length } + + appendLine("์‹คํ–‰ ์‹œ๊ฐ„ ํ†ต๊ณ„:") + appendLine(" ํ‰๊ท : ${executionTimes.average().roundToInt()}ms") + appendLine(" ์ตœ์†Œ: ${executionTimes.minOrNull() ?: 0}ms") + appendLine(" ์ตœ๋Œ€: ${executionTimes.maxOrNull() ?: 0}ms") + appendLine() + + appendLine("์ˆ˜์‹ ๊ธธ์ด ํ†ต๊ณ„:") + appendLine(" ํ‰๊ท : ${formulaLengths.average().roundToInt()}์ž") + appendLine(" ์ตœ์†Œ: ${formulaLengths.minOrNull() ?: 0}์ž") + appendLine(" ์ตœ๋Œ€: ${formulaLengths.maxOrNull() ?: 0}์ž") + appendLine() + } + + appendLine("๋ณด๊ณ ์„œ ์ƒ์„ฑ ์™„๋ฃŒ") + } + } catch (e: Exception) { + throw ExpresserException.reportGenerationError(e) + } + } + + /** + * HTML ๋ฌธ์ž์—ด์„ ์ด์Šค์ผ€์ดํ”„ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun escapeHtml(text: String): String { + return text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + } + + /** + * JSON ๋ฌธ์ž์—ด์„ ์ด์Šค์ผ€์ดํ”„ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun escapeJson(text: String): String { + return text.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + } + + /** + * ์ƒˆ๋กœ์šด ์˜ต์…˜์œผ๋กœ ๋ฆฌํฌํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun withOptions( + newFormatter: ExpressionFormatter = formatter, + newIncludeSteps: Boolean = includeSteps, + newIncludeStatistics: Boolean = includeStatistics, + newIncludeAST: Boolean = includeAST, + newMaxDepth: Int = maxDepth + ): ExpressionReporter { + return ExpressionReporter(newFormatter, newIncludeSteps, newIncludeStatistics, newIncludeAST, newMaxDepth) + } + + /** + * ํ˜„์žฌ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getConfiguration(): Map = mapOf( + "formatterStyle" to formatter.getOptions().style.name, + "includeSteps" to includeSteps, + "includeStatistics" to includeStatistics, + "includeAST" to includeAST, + "maxDepth" to maxDepth + ) + + /** + * ๋ฆฌํฌํ„ฐ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getReporterStatistics(): Map = mapOf( + "supportedFormats" to listOf("TEXT", "HTML", "JSON", "XML"), + "configuration" to getConfiguration(), + "formatterStatistics" to formatter.getFormatterStatistics() + ) + + companion object { + /** + * ๊ธฐ๋ณธ ๋ฆฌํฌํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createDefault(): ExpressionReporter = ExpressionReporter() + + /** + * ๊ฐ„๋‹จํ•œ ๋ฆฌํฌํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createSimple(): ExpressionReporter = ExpressionReporter( + includeSteps = false, + includeStatistics = false, + includeAST = false + ) + + /** + * ์ƒ์„ธํ•œ ๋ฆฌํฌํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createDetailed(): ExpressionReporter = ExpressionReporter( + includeSteps = true, + includeStatistics = true, + includeAST = true + ) + + /** + * ํŠน์ • ์Šคํƒ€์ผ์˜ ๋ฆฌํฌํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createWithStyle(style: FormattingStyle): ExpressionReporter = ExpressionReporter( + formatter = ExpressionFormatter(FormattingOptions.forStyle(style)) + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/entities/FormattingOptions.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/entities/FormattingOptions.kt new file mode 100644 index 00000000..c7a6c1fd --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/entities/FormattingOptions.kt @@ -0,0 +1,431 @@ +package hs.kr.entrydsm.domain.expresser.entities + +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ์ˆ˜์‹ ํฌ๋งทํŒ… ์˜ต์…˜์„ ์ •์˜ํ•˜๋Š” ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. + * + * ํฌ๋งทํŒ… ์Šคํƒ€์ผ๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜์–ด ์ˆ˜์‹ ์ถœ๋ ฅ์˜ ์„ธ๋ถ€์ ์ธ ์„ค์ •์„ ์ œ์–ดํ•ฉ๋‹ˆ๋‹ค. + * ์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜, ๊ณต๋ฐฑ ์ฒ˜๋ฆฌ, ๊ด„ํ˜ธ ์‚ฌ์šฉ ๋“ฑ ๋‹ค์–‘ํ•œ ํฌ๋งทํŒ… ์˜ต์…˜์„ ์ œ๊ณตํ•˜๋ฉฐ, + * ๊ฐ ์Šคํƒ€์ผ์— ๋งž๋Š” ๊ธฐ๋ณธ ์„ค์ •์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * + * @property style ํฌ๋งทํŒ… ์Šคํƒ€์ผ + * @property decimalPlaces ์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜ + * @property addSpaces ๊ณต๋ฐฑ ์ถ”๊ฐ€ ์—ฌ๋ถ€ + * @property showIntegerAsDecimal ์ •์ˆ˜๋ฅผ ์†Œ์ˆ˜์ ์œผ๋กœ ํ‘œ์‹œํ• ์ง€ ์—ฌ๋ถ€ + * @property removeTrailingZeros ๋’ค์ชฝ 0 ์ œ๊ฑฐ ์—ฌ๋ถ€ + * @property useParentheses ๊ด„ํ˜ธ ์‚ฌ์šฉ ์—ฌ๋ถ€ + * @property showOperatorSpacing ์—ฐ์‚ฐ์ž ๊ณต๋ฐฑ ํ‘œ์‹œ ์—ฌ๋ถ€ + * @property preferUnicodeSymbols ์œ ๋‹ˆ์ฝ”๋“œ ๊ธฐํ˜ธ ์‚ฌ์šฉ ์„ ํ˜ธ ์—ฌ๋ถ€ + * @property compactFunctionCalls ํ•จ์ˆ˜ ํ˜ธ์ถœ ์••์ถ• ํ‘œ์‹œ ์—ฌ๋ถ€ + * @property showFullPrecision ์ „์ฒด ์ •๋ฐ€๋„ ํ‘œ์‹œ ์—ฌ๋ถ€ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity( + aggregateRoot = hs.kr.entrydsm.domain.expresser.aggregates.ExpressionFormatter::class, + context = "expresser" +) +data class FormattingOptions( + val style: FormattingStyle, + val decimalPlaces: Int, + val addSpaces: Boolean, + val showIntegerAsDecimal: Boolean, + val removeTrailingZeros: Boolean, + val useParentheses: Boolean, + val showOperatorSpacing: Boolean, + val preferUnicodeSymbols: Boolean, + val compactFunctionCalls: Boolean, + val showFullPrecision: Boolean +) { + + init { + if (decimalPlaces < 0) { + throw hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException.invalidFormatOption( + "decimalPlaces=$decimalPlaces", "์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + } + if (decimalPlaces > 15) { + throw hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException.invalidFormatOption( + "decimalPlaces=$decimalPlaces", "์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜๋Š” 15 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + } + } + + /** + * ์Šคํƒ€์ผ์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newStyle ์ƒˆ๋กœ์šด ์Šคํƒ€์ผ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withStyle(newStyle: FormattingStyle): FormattingOptions = copy(style = newStyle) + + /** + * ์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜๋ฅผ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param places ์ƒˆ๋กœ์šด ์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withDecimalPlaces(places: Int): FormattingOptions { + if (places < 0) { + throw hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException.invalidFormatOption( + "decimalPlaces=$places", "์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + } + if (places > 15) { + throw hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException.invalidFormatOption( + "decimalPlaces=$places", "์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜๋Š” 15 ์ดํ•˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + } + return copy(decimalPlaces = places) + } + + /** + * ๊ณต๋ฐฑ ์ถ”๊ฐ€ ์„ค์ •์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param add ๊ณต๋ฐฑ ์ถ”๊ฐ€ ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withSpaces(add: Boolean): FormattingOptions = copy(addSpaces = add) + + /** + * ์ •์ˆ˜ ์†Œ์ˆ˜์  ํ‘œ์‹œ ์„ค์ •์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param show ์ •์ˆ˜๋ฅผ ์†Œ์ˆ˜์ ์œผ๋กœ ํ‘œ์‹œํ• ์ง€ ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withIntegerAsDecimal(show: Boolean): FormattingOptions = copy(showIntegerAsDecimal = show) + + /** + * ๋’ค์ชฝ 0 ์ œ๊ฑฐ ์„ค์ •์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param remove ๋’ค์ชฝ 0 ์ œ๊ฑฐ ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withTrailingZerosRemoved(remove: Boolean): FormattingOptions = copy(removeTrailingZeros = remove) + + /** + * ๊ด„ํ˜ธ ์‚ฌ์šฉ ์„ค์ •์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param use ๊ด„ํ˜ธ ์‚ฌ์šฉ ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withParentheses(use: Boolean): FormattingOptions = copy(useParentheses = use) + + /** + * ์—ฐ์‚ฐ์ž ๊ณต๋ฐฑ ํ‘œ์‹œ ์„ค์ •์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param show ์—ฐ์‚ฐ์ž ๊ณต๋ฐฑ ํ‘œ์‹œ ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withOperatorSpacing(show: Boolean): FormattingOptions = copy(showOperatorSpacing = show) + + /** + * ์œ ๋‹ˆ์ฝ”๋“œ ๊ธฐํ˜ธ ์‚ฌ์šฉ ์„ค์ •์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param prefer ์œ ๋‹ˆ์ฝ”๋“œ ๊ธฐํ˜ธ ์‚ฌ์šฉ ์„ ํ˜ธ ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withUnicodeSymbols(prefer: Boolean): FormattingOptions = copy(preferUnicodeSymbols = prefer) + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ์••์ถ• ํ‘œ์‹œ ์„ค์ •์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param compact ํ•จ์ˆ˜ ํ˜ธ์ถœ ์••์ถ• ํ‘œ์‹œ ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withCompactFunctionCalls(compact: Boolean): FormattingOptions = copy(compactFunctionCalls = compact) + + /** + * ์ „์ฒด ์ •๋ฐ€๋„ ํ‘œ์‹œ ์„ค์ •์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param show ์ „์ฒด ์ •๋ฐ€๋„ ํ‘œ์‹œ ์—ฌ๋ถ€ + * @return ์ƒˆ๋กœ์šด FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun withFullPrecision(show: Boolean): FormattingOptions = copy(showFullPrecision = show) + + /** + * ์Šคํƒ€์ผ์— ๋”ฐ๋ผ ์ž๋™์œผ๋กœ ์กฐ์ •๋œ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์Šคํƒ€์ผ์— ๋งž๊ฒŒ ์กฐ์ •๋œ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun adjustForStyle(): FormattingOptions = when (style) { + FormattingStyle.MATHEMATICAL -> copy( + addSpaces = true, + preferUnicodeSymbols = true, + useParentheses = false, + showOperatorSpacing = true, + compactFunctionCalls = false + ) + FormattingStyle.PROGRAMMING -> copy( + addSpaces = true, + preferUnicodeSymbols = false, + useParentheses = true, + showOperatorSpacing = true, + compactFunctionCalls = false + ) + FormattingStyle.LATEX -> copy( + addSpaces = true, + preferUnicodeSymbols = false, + useParentheses = false, + showOperatorSpacing = true, + compactFunctionCalls = false + ) + FormattingStyle.COMPACT -> copy( + addSpaces = false, + preferUnicodeSymbols = false, + useParentheses = false, + showOperatorSpacing = false, + compactFunctionCalls = true + ) + FormattingStyle.VERBOSE -> copy( + addSpaces = true, + preferUnicodeSymbols = false, + useParentheses = true, + showOperatorSpacing = true, + compactFunctionCalls = false + ) + } + + /** + * ์Šคํƒ€์ผ๊ณผ ์˜ต์…˜์ด ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์Šคํƒ€์ผ๊ณผ ์˜ต์…˜์ด ์ผ์น˜ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isConsistentWithStyle(): Boolean { + val adjusted = adjustForStyle() + return this.copy(style = adjusted.style) == adjusted + } + + /** + * ์˜ต์…˜์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValid(): Boolean = try { + decimalPlaces >= 0 && decimalPlaces <= 15 + } catch (e: Exception) { + false + } + + /** + * ์˜ต์…˜์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ต์…˜ ์ •๋ณด ๋งต + */ + fun getOptionsInfo(): Map = mapOf( + "style" to style.name, + "decimalPlaces" to decimalPlaces, + "addSpaces" to addSpaces, + "showIntegerAsDecimal" to showIntegerAsDecimal, + "removeTrailingZeros" to removeTrailingZeros, + "useParentheses" to useParentheses, + "showOperatorSpacing" to showOperatorSpacing, + "preferUnicodeSymbols" to preferUnicodeSymbols, + "compactFunctionCalls" to compactFunctionCalls, + "showFullPrecision" to showFullPrecision, + "isConsistentWithStyle" to isConsistentWithStyle(), + "isValid" to isValid() + ) + + /** + * ์˜ต์…˜์„ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ต์…˜ ์„ค๋ช… ๋ฌธ์ž์—ด + */ + override fun toString(): String = buildString { + append("FormattingOptions(") + append("style=${style.name}, ") + append("decimalPlaces=$decimalPlaces, ") + append("addSpaces=$addSpaces, ") + append("showIntegerAsDecimal=$showIntegerAsDecimal, ") + append("removeTrailingZeros=$removeTrailingZeros, ") + append("useParentheses=$useParentheses, ") + append("showOperatorSpacing=$showOperatorSpacing, ") + append("preferUnicodeSymbols=$preferUnicodeSymbols, ") + append("compactFunctionCalls=$compactFunctionCalls, ") + append("showFullPrecision=$showFullPrecision") + append(")") + } + + companion object { + /** + * ๊ธฐ๋ณธ ํฌ๋งทํŒ… ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun default(): FormattingOptions = FormattingOptions( + style = FormattingStyle.MATHEMATICAL, + decimalPlaces = 6, + addSpaces = true, + showIntegerAsDecimal = false, + removeTrailingZeros = true, + useParentheses = false, + showOperatorSpacing = true, + preferUnicodeSymbols = true, + compactFunctionCalls = false, + showFullPrecision = false + ) + + /** + * ์ˆ˜ํ•™์  ์Šคํƒ€์ผ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆ˜ํ•™์  ์Šคํƒ€์ผ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun mathematical(): FormattingOptions = FormattingOptions( + style = FormattingStyle.MATHEMATICAL, + decimalPlaces = 6, + addSpaces = true, + showIntegerAsDecimal = false, + removeTrailingZeros = true, + useParentheses = false, + showOperatorSpacing = true, + preferUnicodeSymbols = true, + compactFunctionCalls = false, + showFullPrecision = false + ) + + /** + * ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์Šคํƒ€์ผ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun programming(): FormattingOptions = FormattingOptions( + style = FormattingStyle.PROGRAMMING, + decimalPlaces = 6, + addSpaces = true, + showIntegerAsDecimal = false, + removeTrailingZeros = true, + useParentheses = true, + showOperatorSpacing = true, + preferUnicodeSymbols = false, + compactFunctionCalls = false, + showFullPrecision = false + ) + + /** + * LaTeX ์Šคํƒ€์ผ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return LaTeX ์Šคํƒ€์ผ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun latex(): FormattingOptions = FormattingOptions( + style = FormattingStyle.LATEX, + decimalPlaces = 6, + addSpaces = true, + showIntegerAsDecimal = false, + removeTrailingZeros = true, + useParentheses = false, + showOperatorSpacing = true, + preferUnicodeSymbols = false, + compactFunctionCalls = false, + showFullPrecision = false + ) + + /** + * ๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun compact(): FormattingOptions = FormattingOptions( + style = FormattingStyle.COMPACT, + decimalPlaces = 3, + addSpaces = false, + showIntegerAsDecimal = false, + removeTrailingZeros = true, + useParentheses = false, + showOperatorSpacing = false, + preferUnicodeSymbols = false, + compactFunctionCalls = true, + showFullPrecision = false + ) + + /** + * ์ƒ์„ธํ•œ ์Šคํƒ€์ผ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธํ•œ ์Šคํƒ€์ผ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun verbose(): FormattingOptions = FormattingOptions( + style = FormattingStyle.VERBOSE, + decimalPlaces = 8, + addSpaces = true, + showIntegerAsDecimal = true, + removeTrailingZeros = false, + useParentheses = true, + showOperatorSpacing = true, + preferUnicodeSymbols = false, + compactFunctionCalls = false, + showFullPrecision = true + ) + + /** + * ๊ณ ์ •๋ฐ€๋„ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณ ์ •๋ฐ€๋„ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun highPrecision(): FormattingOptions = default().copy( + decimalPlaces = 15, + showFullPrecision = true, + removeTrailingZeros = false + ) + + /** + * ์ €์ •๋ฐ€๋„ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ €์ •๋ฐ€๋„ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun lowPrecision(): FormattingOptions = default().copy( + decimalPlaces = 2, + showFullPrecision = false, + removeTrailingZeros = true + ) + + /** + * ํŠน์ • ์Šคํƒ€์ผ์˜ ๊ธฐ๋ณธ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param style ์Šคํƒ€์ผ + * @return ํ•ด๋‹น ์Šคํƒ€์ผ์˜ ๊ธฐ๋ณธ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun forStyle(style: FormattingStyle): FormattingOptions = when (style) { + FormattingStyle.MATHEMATICAL -> mathematical() + FormattingStyle.PROGRAMMING -> programming() + FormattingStyle.LATEX -> latex() + FormattingStyle.COMPACT -> compact() + FormattingStyle.VERBOSE -> verbose() + } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param style ์Šคํƒ€์ผ + * @param decimalPlaces ์†Œ์ˆ˜์  ์ž๋ฆฟ์ˆ˜ + * @param addSpaces ๊ณต๋ฐฑ ์ถ”๊ฐ€ ์—ฌ๋ถ€ + * @param showIntegerAsDecimal ์ •์ˆ˜๋ฅผ ์†Œ์ˆ˜์ ์œผ๋กœ ํ‘œ์‹œํ• ์ง€ ์—ฌ๋ถ€ + * @param removeTrailingZeros ๋’ค์ชฝ 0 ์ œ๊ฑฐ ์—ฌ๋ถ€ + * @return ์‚ฌ์šฉ์ž ์ •์˜ FormattingOptions ์ธ์Šคํ„ด์Šค + */ + fun custom( + style: FormattingStyle = FormattingStyle.MATHEMATICAL, + decimalPlaces: Int = 6, + addSpaces: Boolean = true, + showIntegerAsDecimal: Boolean = false, + removeTrailingZeros: Boolean = true + ): FormattingOptions = FormattingOptions( + style = style, + decimalPlaces = decimalPlaces, + addSpaces = addSpaces, + showIntegerAsDecimal = showIntegerAsDecimal, + removeTrailingZeros = removeTrailingZeros, + useParentheses = when(style) { + FormattingStyle.PROGRAMMING, FormattingStyle.VERBOSE -> true + else -> false + }, + showOperatorSpacing = style != FormattingStyle.COMPACT, + preferUnicodeSymbols = style == FormattingStyle.MATHEMATICAL, + compactFunctionCalls = style == FormattingStyle.COMPACT, + showFullPrecision = style == FormattingStyle.VERBOSE + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/entities/FormattingStyle.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/entities/FormattingStyle.kt new file mode 100644 index 00000000..27749bcb --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/entities/FormattingStyle.kt @@ -0,0 +1,271 @@ +package hs.kr.entrydsm.domain.expresser.entities + +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ์ˆ˜์‹ ํฌ๋งทํŒ… ์Šคํƒ€์ผ์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜• ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. + * + * ๋‹ค์–‘ํ•œ ์ถœ๋ ฅ ํ˜•ํƒœ์™€ ์‚ฌ์šฉ ํ™˜๊ฒฝ์— ๋งž์ถฐ ์ˆ˜์‹์„ ํ‘œํ˜„ํ•˜๋Š” ๋ฐฉ์‹์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ ์Šคํƒ€์ผ์€ ๊ณ ์œ ํ•œ ํ‘œ๊ธฐ๋ฒ•๊ณผ ๊ทœ์น™์„ ๊ฐ€์ง€๋ฉฐ, ํŠน์ • ์šฉ๋„์— ์ตœ์ ํ™”๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @property displayName ์Šคํƒ€์ผ์˜ ํ‘œ์‹œ ์ด๋ฆ„ + * @property description ์Šคํƒ€์ผ์— ๋Œ€ํ•œ ์„ค๋ช… + * @property example ์Šคํƒ€์ผ ์ ์šฉ ์˜ˆ์‹œ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity( + aggregateRoot = hs.kr.entrydsm.domain.expresser.aggregates.ExpressionFormatter::class, + context = "expresser" +) +enum class FormattingStyle( + val displayName: String, + val description: String, + val example: String +) { + /** + * ์ˆ˜ํ•™์  ํ‘œ๊ธฐ๋ฒ• ์Šคํƒ€์ผ + * + * ์ „ํ†ต์ ์ธ ์ˆ˜ํ•™ ํ‘œ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์œ ๋‹ˆ์ฝ”๋“œ ์ˆ˜ํ•™ ๊ธฐํ˜ธ๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ€๋…์„ฑ์ด ๋†’๊ณ  ์ง๊ด€์ ์ธ ํ‘œํ˜„์ด ํŠน์ง•์ž…๋‹ˆ๋‹ค. + */ + MATHEMATICAL( + displayName = "์ˆ˜ํ•™์ ", + description = "์ „ํ†ต์ ์ธ ์ˆ˜ํ•™ ํ‘œ๊ธฐ๋ฒ•์„ ์‚ฌ์šฉํ•˜๋ฉฐ ์œ ๋‹ˆ์ฝ”๋“œ ์ˆ˜ํ•™ ๊ธฐํ˜ธ๋ฅผ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค", + example = "x ร— y + โˆš(zยฒ + 1) โ‰ค ฯ€" + ), + + /** + * ํ”„๋กœ๊ทธ๋ž˜๋ฐ ํ‘œ๊ธฐ๋ฒ• ์Šคํƒ€์ผ + * + * ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ‘œ๊ธฐ๋ฒ•์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค. + * ํ‚ค๋ณด๋“œ๋กœ ์ž…๋ ฅ ๊ฐ€๋Šฅํ•œ ๋ฌธ์ž๋งŒ์„ ์‚ฌ์šฉํ•˜๋ฉฐ, ์ฝ”๋“œ ์ž‘์„ฑ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ + PROGRAMMING( + displayName = "ํ”„๋กœ๊ทธ๋ž˜๋ฐ", + description = "ํ”„๋กœ๊ทธ๋ž˜๋ฐ ์–ธ์–ด์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ‘œ๊ธฐ๋ฒ•์„ ๋”ฐ๋ฆ…๋‹ˆ๋‹ค", + example = "x * y + sqrt(z^2 + 1) <= 3.14159" + ), + + /** + * LaTeX ํ‘œ๊ธฐ๋ฒ• ์Šคํƒ€์ผ + * + * LaTeX ๋ฌธ์„œ ์ž‘์„ฑ ์‹œ ์‚ฌ์šฉ๋˜๋Š” ํ‘œ๊ธฐ๋ฒ•์ž…๋‹ˆ๋‹ค. + * ํ•™์ˆ  ๋…ผ๋ฌธ์ด๋‚˜ ์ „๋ฌธ ๋ฌธ์„œ ์ž‘์„ฑ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ + LATEX( + displayName = "LaTeX", + description = "LaTeX ๋ฌธ์„œ ์ž‘์„ฑ ์‹œ ์‚ฌ์šฉ๋˜๋Š” ํ‘œ๊ธฐ๋ฒ•์ž…๋‹ˆ๋‹ค", + example = "x \\times y + \\sqrt{z^{2} + 1} \\leq \\pi" + ), + + /** + * ๊ฐ„๊ฒฐํ•œ ํ‘œ๊ธฐ๋ฒ• ์Šคํƒ€์ผ + * + * ์ตœ์†Œํ•œ์˜ ๋ฌธ์ž๋กœ ํ‘œํ˜„ํ•˜๋Š” ๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ์ž…๋‹ˆ๋‹ค. + * ๊ณต๊ฐ„์ด ์ œํ•œ๋œ ํ™˜๊ฒฝ์ด๋‚˜ ๋น ๋ฅธ ์ž…๋ ฅ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ + COMPACT( + displayName = "๊ฐ„๊ฒฐํ•œ", + description = "์ตœ์†Œํ•œ์˜ ๋ฌธ์ž๋กœ ํ‘œํ˜„ํ•˜๋Š” ๊ฐ„๊ฒฐํ•œ ์Šคํƒ€์ผ์ž…๋‹ˆ๋‹ค", + example = "x*y+sqrt(z^2+1)<=pi" + ), + + /** + * ์ƒ์„ธํ•œ ํ‘œ๊ธฐ๋ฒ• ์Šคํƒ€์ผ + * + * ๋ชจ๋“  ์—ฐ์‚ฐ๊ณผ ๊ตฌ์กฐ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” ์ƒ์„ธํ•œ ์Šคํƒ€์ผ์ž…๋‹ˆ๋‹ค. + * ๊ต์œก์šฉ์ด๋‚˜ ๋ช…ํ™•ํ•œ ์„ค๋ช…์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ์— ์ ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ + VERBOSE( + displayName = "์ƒ์„ธํ•œ", + description = "๋ชจ๋“  ์—ฐ์‚ฐ๊ณผ ๊ตฌ์กฐ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” ์ƒ์„ธํ•œ ์Šคํƒ€์ผ์ž…๋‹ˆ๋‹ค", + example = "(๋ณ€์ˆ˜(x) ๊ณฑํ•˜๊ธฐ ๋ณ€์ˆ˜(y)) ๋”ํ•˜๊ธฐ ํ•จ์ˆ˜_์ œ๊ณฑ๊ทผ(๋ณ€์ˆ˜(z) ๊ฑฐ๋“ญ์ œ๊ณฑ 2 ๋”ํ•˜๊ธฐ 1) ์ž‘๊ฑฐ๋‚˜ ๊ฐ™๋‹ค ฯ€" + ); + + /** + * ์Šคํƒ€์ผ์ด ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๋ฅผ ์‚ฌ์šฉํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž ์‚ฌ์šฉ ์‹œ true, ์•„๋‹ˆ๋ฉด false + */ + fun usesUnicodeCharacters(): Boolean = when (this) { + MATHEMATICAL -> true + PROGRAMMING -> false + LATEX -> false + COMPACT -> false + VERBOSE -> false + } + + /** + * ์Šคํƒ€์ผ์ด ๊ณต๋ฐฑ์„ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณต๋ฐฑ ํฌํ•จ ์‹œ true, ์•„๋‹ˆ๋ฉด false + */ + fun includesSpaces(): Boolean = when (this) { + MATHEMATICAL -> true + PROGRAMMING -> true + LATEX -> true + COMPACT -> false + VERBOSE -> true + } + + /** + * ์Šคํƒ€์ผ์ด ๊ด„ํ˜ธ๋ฅผ ์ž์ฃผ ์‚ฌ์šฉํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ด„ํ˜ธ ์ž์ฃผ ์‚ฌ์šฉ ์‹œ true, ์•„๋‹ˆ๋ฉด false + */ + fun usesParenthesesFrequently(): Boolean = when (this) { + MATHEMATICAL -> false + PROGRAMMING -> true + LATEX -> false + COMPACT -> false + VERBOSE -> true + } + + /** + * ์Šคํƒ€์ผ์ด ํŠน์ˆ˜ ๋ฌธ์ž๋ฅผ ์‚ฌ์šฉํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŠน์ˆ˜ ๋ฌธ์ž ์‚ฌ์šฉ ์‹œ true, ์•„๋‹ˆ๋ฉด false + */ + fun usesSpecialCharacters(): Boolean = when (this) { + MATHEMATICAL -> true + PROGRAMMING -> false + LATEX -> true + COMPACT -> false + VERBOSE -> false + } + + /** + * ์Šคํƒ€์ผ์˜ ๊ธธ์ด ํŠน์„ฑ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธธ์ด ํŠน์„ฑ (์งง์Œ, ๋ณดํ†ต, ๊ธบ) + */ + fun getLengthCharacteristic(): String = when (this) { + MATHEMATICAL -> "๋ณดํ†ต" + PROGRAMMING -> "๋ณดํ†ต" + LATEX -> "๊ธบ" + COMPACT -> "์งง์Œ" + VERBOSE -> "๊ธบ" + } + + /** + * ์Šคํƒ€์ผ์˜ ์ฃผ์š” ์‚ฌ์šฉ ๋ถ„์•ผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฃผ์š” ์‚ฌ์šฉ ๋ถ„์•ผ ๋ชฉ๋ก + */ + fun getPrimaryUseCases(): List = when (this) { + MATHEMATICAL -> listOf("์ˆ˜ํ•™ ๊ต์œก", "์ผ๋ฐ˜ ๋ฌธ์„œ", "ํ‘œ์ค€ ํ‘œ๊ธฐ") + PROGRAMMING -> listOf("์†Œํ”„ํŠธ์›จ์–ด ๊ฐœ๋ฐœ", "์ฝ”๋“œ ์ƒ์„ฑ", "API ๋ฌธ์„œ") + LATEX -> listOf("ํ•™์ˆ  ๋…ผ๋ฌธ", "์ˆ˜ํ•™ ๋ฌธ์„œ", "์ถœํŒ๋ฌผ") + COMPACT -> listOf("์ œํ•œ๋œ ๊ณต๊ฐ„", "๋น ๋ฅธ ์ž…๋ ฅ", "๊ฐ„๋‹จํ•œ ํ‘œ์‹œ") + VERBOSE -> listOf("๊ต์œก์šฉ", "์ƒ์„ธ ์„ค๋ช…", "๋””๋ฒ„๊น…") + } + + /** + * ์Šคํƒ€์ผ์˜ ๊ฐ€๋…์„ฑ ์ˆ˜์ค€์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ€๋…์„ฑ ์ˆ˜์ค€ (1-5, ๋†’์„์ˆ˜๋ก ๊ฐ€๋…์„ฑ์ด ์ข‹์Œ) + */ + fun getReadabilityLevel(): Int = when (this) { + MATHEMATICAL -> 5 + PROGRAMMING -> 4 + LATEX -> 3 + COMPACT -> 2 + VERBOSE -> 4 + } + + /** + * ์Šคํƒ€์ผ์˜ ์ž…๋ ฅ ํŽธ์˜์„ฑ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ž…๋ ฅ ํŽธ์˜์„ฑ (1-5, ๋†’์„์ˆ˜๋ก ์ž…๋ ฅํ•˜๊ธฐ ์‰ฌ์›€) + */ + fun getInputConvenience(): Int = when (this) { + MATHEMATICAL -> 2 + PROGRAMMING -> 5 + LATEX -> 3 + COMPACT -> 4 + VERBOSE -> 1 + } + + /** + * ์Šคํƒ€์ผ์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์Šคํƒ€์ผ ์ •๋ณด ๋งต + */ + fun getStyleInfo(): Map = mapOf( + "name" to name, + "displayName" to displayName, + "description" to description, + "example" to example, + "usesUnicode" to usesUnicodeCharacters(), + "includesSpaces" to includesSpaces(), + "usesParentheses" to usesParenthesesFrequently(), + "usesSpecialChars" to usesSpecialCharacters(), + "lengthCharacteristic" to getLengthCharacteristic(), + "primaryUseCases" to getPrimaryUseCases(), + "readabilityLevel" to getReadabilityLevel(), + "inputConvenience" to getInputConvenience() + ) + + companion object { + /** + * ๊ธฐ๋ณธ ์Šคํƒ€์ผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์Šคํƒ€์ผ (MATHEMATICAL) + */ + fun default(): FormattingStyle = MATHEMATICAL + + /** + * ํ‘œ์‹œ ์ด๋ฆ„์œผ๋กœ ์Šคํƒ€์ผ์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param displayName ์ฐพ์„ ํ‘œ์‹œ ์ด๋ฆ„ + * @return ํ•ด๋‹น ์Šคํƒ€์ผ ๋˜๋Š” null + */ + fun findByDisplayName(displayName: String): FormattingStyle? = + values().find { it.displayName == displayName } + + /** + * ๊ฐ€๋…์„ฑ์ด ๊ฐ€์žฅ ๋†’์€ ์Šคํƒ€์ผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ€์žฅ ๊ฐ€๋…์„ฑ์ด ๋†’์€ ์Šคํƒ€์ผ + */ + fun mostReadable(): FormattingStyle = + values().maxByOrNull { it.getReadabilityLevel() } ?: MATHEMATICAL + + /** + * ์ž…๋ ฅ์ด ๊ฐ€์žฅ ํŽธ๋ฆฌํ•œ ์Šคํƒ€์ผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ€์žฅ ์ž…๋ ฅํ•˜๊ธฐ ํŽธ๋ฆฌํ•œ ์Šคํƒ€์ผ + */ + fun mostInputFriendly(): FormattingStyle = + values().maxByOrNull { it.getInputConvenience() } ?: PROGRAMMING + + /** + * ํŠน์ • ์‚ฌ์šฉ ๋ถ„์•ผ์— ์ ํ•ฉํ•œ ์Šคํƒ€์ผ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param useCase ์‚ฌ์šฉ ๋ถ„์•ผ + * @return ํ•ด๋‹น ๋ถ„์•ผ์— ์ ํ•ฉํ•œ ์Šคํƒ€์ผ๋“ค + */ + fun getStylesForUseCase(useCase: String): List = + values().filter { it.getPrimaryUseCases().contains(useCase) } + + /** + * ๋ชจ๋“  ์Šคํƒ€์ผ์˜ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์Šคํƒ€์ผ ํ†ต๊ณ„ ๋งต + */ + fun getStatistics(): Map = mapOf( + "totalStyles" to values().size, + "unicodeStyles" to values().count { it.usesUnicodeCharacters() }, + "spacedStyles" to values().count { it.includesSpaces() }, + "parenthesesStyles" to values().count { it.usesParenthesesFrequently() }, + "specialCharStyles" to values().count { it.usesSpecialCharacters() }, + "averageReadability" to values().map { it.getReadabilityLevel() }.average(), + "averageInputConvenience" to values().map { it.getInputConvenience() }.average(), + "lengthDistribution" to values().groupingBy { it.getLengthCharacteristic() }.eachCount() + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/exceptions/ExpresserException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/exceptions/ExpresserException.kt new file mode 100644 index 00000000..55c128e5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/exceptions/ExpresserException.kt @@ -0,0 +1,237 @@ +package hs.kr.entrydsm.domain.expresser.exceptions + +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * Expresser ๋„๋ฉ”์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ๊ฒฐ๊ณผ ํฌ๋งทํŒ…, ์ถœ๋ ฅ ์ƒ์„ฑ, ํ‘œํ˜„ ํ˜•์‹ ๋ณ€ํ™˜ ๋“ฑ์˜ ํ‘œํ˜„ ๊ณ„์ธต์—์„œ + * ๋ฐœ์ƒํ•˜๋Š” ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @property format ์˜ค๋ฅ˜์™€ ๊ด€๋ จ๋œ ํฌ๋งท (์„ ํƒ์‚ฌํ•ญ) + * @property option ์˜ค๋ฅ˜์™€ ๊ด€๋ จ๋œ ํฌ๋งท ์˜ต์…˜ (์„ ํƒ์‚ฌํ•ญ) + * @property outputType ์ถœ๋ ฅ ํƒ€์ž… (์„ ํƒ์‚ฌํ•ญ) + * @property data ํฌ๋งทํŒ… ๋Œ€์ƒ ๋ฐ์ดํ„ฐ (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +class ExpresserException( + errorCode: ErrorCode, + val format: String? = null, + val option: String? = null, + val outputType: String? = null, + val data: Any? = null, + message: String = buildExpresserMessage(errorCode, format, option, outputType, data), + cause: Throwable? = null +) : DomainException(errorCode, message, cause) { + + companion object { + /** + * Expresser ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param errorCode ์˜ค๋ฅ˜ ์ฝ”๋“œ + * @param format ํฌ๋งท + * @param option ํฌ๋งท ์˜ต์…˜ + * @param outputType ์ถœ๋ ฅ ํƒ€์ž… + * @param data ๋ฐ์ดํ„ฐ + * @return ๊ตฌ์„ฑ๋œ ๋ฉ”์‹œ์ง€ + */ + private fun buildExpresserMessage( + errorCode: ErrorCode, + format: String?, + option: String?, + outputType: String?, + data: Any? + ): String { + val baseMessage = errorCode.description + val details = mutableListOf() + + format?.let { details.add("ํฌ๋งท: $it") } + option?.let { details.add("์˜ต์…˜: $it") } + outputType?.let { details.add("์ถœ๋ ฅํƒ€์ž…: $it") } + data?.let { + val dataStr = when (data) { + is String -> if (data.length > 50) "${data.take(50)}..." else data + else -> data.toString().let { if (it.length > 50) "${it.take(50)}..." else it } + } + details.add("๋ฐ์ดํ„ฐ: $dataStr") + } + + return if (details.isNotEmpty()) { + "$baseMessage (${details.joinToString(", ")})" + } else { + baseMessage + } + } + + /** + * ํฌ๋งทํŒ… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param format ํฌ๋งท + * @param data ํฌ๋งทํŒ… ๋Œ€์ƒ ๋ฐ์ดํ„ฐ + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return ExpresserException ์ธ์Šคํ„ด์Šค + */ + fun formattingError(format: String, data: Any? = null, cause: Throwable? = null): ExpresserException { + return ExpresserException( + errorCode = ErrorCode.INVALID_INPUT, + format = format, + data = data, + cause = cause + ) + } + + /** + * ์ž˜๋ชป๋œ ํฌ๋งท ์˜ต์…˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param option ์ž˜๋ชป๋œ ํฌ๋งท ์˜ต์…˜ + * @param format ๊ด€๋ จ ํฌ๋งท + * @return ExpresserException ์ธ์Šคํ„ด์Šค + */ + fun invalidFormatOption(option: String, format: String? = null): ExpresserException { + return ExpresserException( + errorCode = ErrorCode.INVALID_INPUT, + option = option, + format = format + ) + } + + /** + * ์ถœ๋ ฅ ์ƒ์„ฑ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param outputType ์ถœ๋ ฅ ํƒ€์ž… + * @param data ์ถœ๋ ฅ ๋Œ€์ƒ ๋ฐ์ดํ„ฐ + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return ExpresserException ์ธ์Šคํ„ด์Šค + */ + fun outputGenerationError(outputType: String, data: Any? = null, cause: Throwable? = null): ExpresserException { + return ExpresserException( + errorCode = ErrorCode.INVALID_INPUT, + outputType = outputType, + data = data, + cause = cause + ) + } + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํฌ๋งท ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param format ์ง€์›ํ•˜์ง€ ์•Š๋Š” ํฌ๋งท + * @return ExpresserException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedFormat(format: String): ExpresserException { + return ExpresserException( + errorCode = ErrorCode.INVALID_INPUT, + format = format + ) + } + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ถœ๋ ฅ ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param outputType ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ถœ๋ ฅ ํƒ€์ž… + * @return ExpresserException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedOutputType(outputType: String): ExpresserException { + return ExpresserException( + errorCode = ErrorCode.INVALID_INPUT, + outputType = outputType + ) + } + + /** + * ๊ฒฐ๊ณผ ํฌ๋งทํŒ… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ํฌ๋งทํŒ… ๋Œ€์ƒ ๊ฒฐ๊ณผ + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return ExpresserException ์ธ์Šคํ„ด์Šค + */ + fun resultFormattingError(result: Any?, cause: Throwable? = null): ExpresserException { + return ExpresserException( + errorCode = ErrorCode.FORMATTING_ERROR, + data = result, + cause = cause + ) + } + + /** + * ๋ณด๊ณ ์„œ ์ƒ์„ฑ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return ExpresserException ์ธ์Šคํ„ด์Šค + */ + fun reportGenerationError(cause: Throwable? = null): ExpresserException { + return ExpresserException( + errorCode = ErrorCode.OUTPUT_GENERATION_ERROR, + cause = cause + ) + } + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์Šคํƒ€์ผ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param style ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์Šคํƒ€์ผ + * @return ExpresserException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedStyle(style: String): ExpresserException { + return ExpresserException( + errorCode = ErrorCode.UNSUPPORTED_STYLE, + format = style + ) + } + + /** + * ์ž˜๋ชป๋œ ๋…ธ๋“œ ํƒ€์ž… ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeType ์ž˜๋ชป๋œ ๋…ธ๋“œ ํƒ€์ž… + * @return ExpresserException ์ธ์Šคํ„ด์Šค + */ + fun invalidNodeType(nodeType: String): ExpresserException { + return ExpresserException( + errorCode = ErrorCode.INVALID_NODE_TYPE, + data = nodeType + ) + } + } + + /** + * Expresser ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํฌ๋งท, ์˜ต์…˜, ์ถœ๋ ฅ ํƒ€์ž…, ๋ฐ์ดํ„ฐ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋งต + */ + fun getExpresserInfo(): Map = mapOf( + "format" to format, + "option" to option, + "outputType" to outputType, + "data" to data + ).filterValues { it != null } + + /** + * ์ „์ฒด ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ์ •๋ณด์™€ Expresser ์ •๋ณด๊ฐ€ ๊ฒฐํ•ฉ๋œ ๋งต + */ + fun toCompleteErrorInfo(): Map { + val baseInfo = super.toErrorInfo().toMutableMap() + val expresserInfo = getExpresserInfo() + + expresserInfo.forEach { (key, value) -> + baseInfo[key] = value?.toString() ?: "" + } + + return baseInfo + } + + override fun toString(): String { + val expresserDetails = getExpresserInfo() + return if (expresserDetails.isNotEmpty()) { + "${super.toString()}, expresser=${expresserDetails}" + } else { + super.toString() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/factories/ExpresserFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/factories/ExpresserFactory.kt new file mode 100644 index 00000000..e921bb5d --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/factories/ExpresserFactory.kt @@ -0,0 +1,441 @@ +package hs.kr.entrydsm.domain.expresser.factories + +import hs.kr.entrydsm.domain.expresser.aggregates.ExpressionFormatter +import hs.kr.entrydsm.domain.expresser.aggregates.ExpressionReporter +import hs.kr.entrydsm.domain.expresser.entities.FormattingOptions +import hs.kr.entrydsm.domain.expresser.entities.FormattingStyle +import hs.kr.entrydsm.domain.expresser.values.FormattedExpression +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import java.time.Instant + +/** + * Expresser ๋„๋ฉ”์ธ ๊ฐ์ฒด๋“ค์„ ์ƒ์„ฑํ•˜๋Š” ํŒฉํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. + * + * DDD Factory ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ‘œํ˜„์‹ ํ˜•์‹ํ™” ๊ด€๋ จ ๊ฐ์ฒด๋“ค์˜ ์ƒ์„ฑ๊ณผ ๊ตฌ์„ฑ์„ + * ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ํ˜•์‹ํ™” ์˜ต์…˜๊ณผ ์Šคํƒ€์ผ์„ ์ง€์›ํ•˜๋ฉฐ + * ์ ์ ˆํ•œ ์„ค์ •๊ณผ ์ •์ฑ…์„ ์ ์šฉํ•˜์—ฌ ์ผ๊ด€๋œ ๊ฐ์ฒด ์ƒ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Factory( + context = "expresser", + complexity = Complexity.NORMAL, + cache = true +) +class ExpresserFactory { + + companion object { + private var createdFormatterCount = 0L + private var createdReporterCount = 0L + private var createdOptionsCount = 0L + + // ๊ธฐ๋ณธ ์„ค์ •๋“ค + private const val DEFAULT_FONT_SIZE = 12 + private const val DEFAULT_LINE_HEIGHT = 1.4 + private const val DEFAULT_MAX_LINE_LENGTH = 80 + + // ์‹ฑ๊ธ€ํ†ค ์ง€์› + @Volatile + private var instance: ExpresserFactory? = null + + /** + * ์‹ฑ๊ธ€ํ†ค ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getInstance(): ExpresserFactory { + return instance ?: synchronized(this) { + instance ?: ExpresserFactory().also { instance = it } + } + } + } + + /** + * ๊ธฐ๋ณธ ํ‘œํ˜„์‹ ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์„ค์ •์˜ ํ‘œํ˜„์‹ ํ˜•์‹ํ™”๊ธฐ + */ + fun createBasicFormatter(): ExpressionFormatter { + createdFormatterCount++ + return ExpressionFormatter(FormattingOptions.default()) + } + + /** + * LaTeX ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LaTeX ์ „์šฉ ํ˜•์‹ํ™”๊ธฐ + */ + fun createLaTeXFormatter(): ExpressionFormatter { + createdFormatterCount++ + return ExpressionFormatter.createLatex() + } + + /** + * MathML ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return MathML ์ „์šฉ ํ˜•์‹ํ™”๊ธฐ + */ + fun createMathMLFormatter(): ExpressionFormatter { + createdFormatterCount++ + return ExpressionFormatter(FormattingOptions.mathematical()) + } + + /** + * HTML ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return HTML ์ „์šฉ ํ˜•์‹ํ™”๊ธฐ + */ + fun createHTMLFormatter(): ExpressionFormatter { + createdFormatterCount++ + return ExpressionFormatter(FormattingOptions.mathematical()) + } + + /** + * ์œ ๋‹ˆ์ฝ”๋“œ ์ˆ˜ํ•™ ๊ธฐํ˜ธ ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ๋‹ˆ์ฝ”๋“œ ์ˆ˜ํ•™ ๊ธฐํ˜ธ ์ „์šฉ ํ˜•์‹ํ™”๊ธฐ + */ + fun createUnicodeFormatter(): ExpressionFormatter { + createdFormatterCount++ + return ExpressionFormatter(FormattingOptions.mathematical()) + } + + /** + * ASCII ์ „์šฉ ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ASCII ์ „์šฉ ํ˜•์‹ํ™”๊ธฐ + */ + fun createASCIIFormatter(): ExpressionFormatter { + createdFormatterCount++ + return ExpressionFormatter(FormattingOptions.programming()) + } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ์‚ฌ์šฉ์ž ์ •์˜ ํ˜•์‹ํ™”๊ธฐ + */ + fun createCustomFormatter(options: FormattingOptions): ExpressionFormatter { + createdFormatterCount++ + return ExpressionFormatter(options) + } + + /** + * ๊ณ ํ’ˆ์งˆ ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณ ํ’ˆ์งˆ ์„ค์ •์˜ ํ˜•์‹ํ™”๊ธฐ + */ + fun createHighQualityFormatter(): ExpressionFormatter { + createdFormatterCount++ + val options = createHighQualityOptions() + return ExpressionFormatter(options) + } + + /** + * ์ ‘๊ทผ์„ฑ ๊ฐ•ํ™” ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ ‘๊ทผ์„ฑ์ด ๊ฐ•ํ™”๋œ ํ˜•์‹ํ™”๊ธฐ + */ + fun createAccessibleFormatter(): ExpressionFormatter { + createdFormatterCount++ + val options = createAccessibleOptions() + return ExpressionFormatter(options) + } + + /** + * ๊ธฐ๋ณธ ํ‘œํ˜„์‹ ๋ฆฌํฌํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์„ค์ •์˜ ํ‘œํ˜„์‹ ๋ฆฌํฌํ„ฐ + */ + fun createBasicReporter(): ExpressionReporter { + createdReporterCount++ + return ExpressionReporter.createDefault() + } + + /** + * ์ƒ์„ธ ๋ถ„์„ ๋ฆฌํฌํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ๋ถ„์„ ๊ธฐ๋Šฅ์ด ํฌํ•จ๋œ ๋ฆฌํฌํ„ฐ + */ + fun createDetailedReporter(): ExpressionReporter { + createdReporterCount++ + return ExpressionReporter.createDetailed() + } + + /** + * ์„ฑ๋Šฅ ๋ถ„์„ ๋ฆฌํฌํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๋Šฅ ๋ถ„์„ ์ „์šฉ ๋ฆฌํฌํ„ฐ + */ + fun createPerformanceReporter(): ExpressionReporter { + createdReporterCount++ + return ExpressionReporter.createSimple() + } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ๋ฆฌํฌํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ์‚ฌ์šฉ์ž ์ •์˜ ๋ฆฌํฌํ„ฐ + */ + fun createCustomReporter(options: FormattingOptions): ExpressionReporter { + createdReporterCount++ + return ExpressionReporter(ExpressionFormatter(options)) + } + + /** + * ๊ธฐ๋ณธ ํ˜•์‹ํ™” ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ํ˜•์‹ํ™” ์˜ต์…˜ + */ + fun createDefaultOptions(): FormattingOptions { + createdOptionsCount++ + return FormattingOptions.default() + } + + /** + * LaTeX ์ „์šฉ ํ˜•์‹ํ™” ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LaTeX ์ „์šฉ ์˜ต์…˜ + */ + fun createLaTeXOptions(): FormattingOptions { + createdOptionsCount++ + return FormattingOptions.latex() + } + + /** + * ์›น ํ‘œ์‹œ์šฉ ํ˜•์‹ํ™” ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์›น ํ‘œ์‹œ์šฉ ์˜ต์…˜ + */ + fun createWebOptions(): FormattingOptions { + createdOptionsCount++ + return FormattingOptions.mathematical() + } + + /** + * ์ธ์‡„์šฉ ํ˜•์‹ํ™” ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ธ์‡„์šฉ ์˜ต์…˜ + */ + fun createPrintOptions(): FormattingOptions { + createdOptionsCount++ + return FormattingOptions.compact() + } + + /** + * ๋ชจ๋ฐ”์ผ์šฉ ํ˜•์‹ํ™” ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ชจ๋ฐ”์ผ์šฉ ์˜ต์…˜ + */ + fun createMobileOptions(): FormattingOptions { + createdOptionsCount++ + return FormattingOptions.mathematical() + } + + /** + * ๊ณ ํ’ˆ์งˆ ํ˜•์‹ํ™” ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณ ํ’ˆ์งˆ ์˜ต์…˜ + */ + fun createHighQualityOptions(): FormattingOptions { + createdOptionsCount++ + return FormattingOptions.highPrecision() + } + + /** + * ์ ‘๊ทผ์„ฑ ๊ฐ•ํ™” ํ˜•์‹ํ™” ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ ‘๊ทผ์„ฑ ๊ฐ•ํ™” ์˜ต์…˜ + */ + fun createAccessibleOptions(): FormattingOptions { + createdOptionsCount++ + return FormattingOptions.verbose() + } + + /** + * ๊ฐœ๋ฐœ์ž์šฉ ํ˜•์‹ํ™” ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐœ๋ฐœ์ž์šฉ ์˜ต์…˜ + */ + fun createDeveloperOptions(): FormattingOptions { + createdOptionsCount++ + return FormattingOptions.programming() + } + + /** + * ํ”„๋ ˆ์  ํ…Œ์ด์…˜์šฉ ํ˜•์‹ํ™” ์˜ต์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ”„๋ ˆ์  ํ…Œ์ด์…˜์šฉ ์˜ต์…˜ + */ + fun createPresentationOptions(): FormattingOptions { + createdOptionsCount++ + return FormattingOptions.mathematical() + } + + /** + * ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param content ๋‚ด์šฉ + * @param format ํ˜•์‹ + * @param metadata ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ + * @return ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + */ + fun createFormattedExpression( + content: String, + format: String, + metadata: Map = emptyMap() + ): FormattedExpression { + return FormattedExpression( + expression = content, + style = FormattingStyle.MATHEMATICAL, + options = FormattingOptions.default() + ) + } + + /** + * ์„ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param content ๋‚ด์šฉ + * @param format ํ˜•์‹ + * @param processingTime ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ + * @return ์„ฑ๊ณต ๊ฒฐ๊ณผ + */ + fun createSuccessResult( + content: String, + format: String, + processingTime: Long = 0 + ): FormattedExpression { + return FormattedExpression( + expression = content, + style = FormattingStyle.MATHEMATICAL, + options = FormattingOptions.default() + ) + } + + /** + * ์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param error ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ + * @param format ์‹œ๋„ํ•œ ํ˜•์‹ + * @return ์‹คํŒจ ๊ฒฐ๊ณผ + */ + fun createFailureResult( + error: String, + format: String + ): FormattedExpression { + return FormattedExpression( + expression = "Error: $error", + style = FormattingStyle.MATHEMATICAL, + options = FormattingOptions.default() + ) + } + + // Style creation methods + + private fun createDefaultStyle(): FormattingStyle { + return FormattingStyle.MATHEMATICAL + } + + private fun createLaTeXStyle(): FormattingStyle { + return FormattingStyle.LATEX + } + + private fun createWebStyle(): FormattingStyle { + return FormattingStyle.MATHEMATICAL + } + + private fun createPrintStyle(): FormattingStyle { + return FormattingStyle.COMPACT + } + + private fun createMobileStyle(): FormattingStyle { + return FormattingStyle.MATHEMATICAL + } + + private fun createHighQualityStyle(): FormattingStyle { + return FormattingStyle.MATHEMATICAL + } + + private fun createAccessibleStyle(): FormattingStyle { + return FormattingStyle.VERBOSE + } + + private fun createDeveloperStyle(): FormattingStyle { + return FormattingStyle.PROGRAMMING + } + + private fun createPresentationStyle(): FormattingStyle { + return FormattingStyle.MATHEMATICAL + } + + /** + * ํ™˜๊ฒฝ๋ณ„ ์ตœ์ ํ™”๋œ ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param environment ํ™˜๊ฒฝ ("web", "mobile", "print", "presentation") + * @return ํ™˜๊ฒฝ์— ์ตœ์ ํ™”๋œ ํ˜•์‹ํ™”๊ธฐ + */ + fun createFormatterForEnvironment(environment: String): ExpressionFormatter { + val options = when (environment.lowercase()) { + "web" -> createWebOptions() + "mobile" -> createMobileOptions() + "print" -> createPrintOptions() + "presentation" -> createPresentationOptions() + else -> createDefaultOptions() + } + return createCustomFormatter(options) + } + + /** + * ์‚ฌ์šฉ์ž ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๋Š” ํ˜•์‹ํ™”๊ธฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param requirements ์š”๊ตฌ์‚ฌํ•ญ ๋งต + * @return ์š”๊ตฌ์‚ฌํ•ญ์— ๋งž๋Š” ํ˜•์‹ํ™”๊ธฐ + */ + fun createFormatterForRequirements(requirements: Map): ExpressionFormatter { + val accessibility = requirements["accessibility"] as? Boolean ?: false + + val options = if (accessibility) { + createAccessibleOptions() + } else { + createDefaultOptions() + } + + return createCustomFormatter(options) + } + + /** + * ํŒฉํ† ๋ฆฌ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "factoryName" to "ExpresserFactory", + "createdFormatters" to createdFormatterCount, + "createdReporters" to createdReporterCount, + "createdOptions" to createdOptionsCount, + "supportedFormats" to listOf("latex", "mathml", "html", "unicode", "ascii", "text"), + "supportedEnvironments" to listOf("web", "mobile", "print", "presentation", "developer"), + "supportedStyles" to listOf("default", "latex", "web", "print", "mobile", "accessible"), + "cacheEnabled" to true, + "complexityLevel" to Complexity.NORMAL.name + ) + + /** + * ํŒฉํ† ๋ฆฌ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "defaultFontSize" to DEFAULT_FONT_SIZE, + "defaultLineHeight" to DEFAULT_LINE_HEIGHT, + "defaultMaxLineLength" to DEFAULT_MAX_LINE_LENGTH, + "supportedFormats" to listOf("infix", "prefix", "postfix", "latex", "mathml", "html", "unicode", "ascii"), + "supportedThemes" to listOf("default", "academic", "modern", "print", "mobile", "accessible", "developer", "presentation"), + "supportedColorSchemes" to listOf("light", "dark", "high-contrast", "monochrome", "auto", "enhanced") + ) + +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/interfaces/ExpresserContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/interfaces/ExpresserContract.kt new file mode 100644 index 00000000..e7b7f27c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/interfaces/ExpresserContract.kt @@ -0,0 +1,279 @@ +package hs.kr.entrydsm.domain.expresser.interfaces + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.expresser.entities.FormattingOptions +import hs.kr.entrydsm.domain.expresser.values.FormattedExpression + +/** + * ํ‘œํ˜„์‹ ์ถœ๋ ฅ์ž์˜ ํ•ต์‹ฌ ๊ณ„์•ฝ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Anti-Corruption Layer ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋‹ค์–‘ํ•œ ํ‘œํ˜„์‹ ์ถœ๋ ฅ ๊ตฌํ˜„์ฒด๋“ค ๊ฐ„์˜ + * ํ˜ธํ™˜์„ฑ์„ ๋ณด์žฅํ•˜๋ฉฐ, ํ‘œํ˜„์‹ ํ˜•์‹ํ™”์™€ ์ถœ๋ ฅ์˜ ํ•ต์‹ฌ ๊ธฐ๋Šฅ์„ ํ‘œ์ค€ํ™”๋œ ๋ฐฉ์‹์œผ๋กœ + * ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. DDD ์ธํ„ฐํŽ˜์ด์Šค ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๊ตฌํ˜„์ฒด์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์˜ + * ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถฅ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface ExpresserContract { + + /** + * AST๋ฅผ ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ณ€ํ™˜ํ•  AST ๋…ธ๋“œ + * @return ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + */ + fun format(ast: ASTNode): FormattedExpression + + /** + * AST๋ฅผ ํŠน์ • ์˜ต์…˜์œผ๋กœ ํ˜•์‹ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ณ€ํ™˜ํ•  AST ๋…ธ๋“œ + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + */ + fun format(ast: ASTNode, options: FormattingOptions): FormattedExpression + + /** + * ํ‘œํ˜„์‹ ๋ฌธ์ž์—ด์„ ์žฌํ˜•์‹ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ์›๋ณธ ํ‘œํ˜„์‹ + * @return ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + */ + fun reformat(expression: String): FormattedExpression + + /** + * ํ‘œํ˜„์‹ ๋ฌธ์ž์—ด์„ ํŠน์ • ์˜ต์…˜์œผ๋กœ ์žฌํ˜•์‹ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ์›๋ณธ ํ‘œํ˜„์‹ + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + */ + fun reformat(expression: String, options: FormattingOptions): FormattedExpression + + /** + * AST๋ฅผ ํŠน์ • ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ์ถœ๋ ฅํ•  AST ๋…ธ๋“œ + * @param format ์ถœ๋ ฅ ํ˜•์‹ ("infix", "prefix", "postfix", "latex", "mathml" ๋“ฑ) + * @return ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + */ + fun express(ast: ASTNode, format: String): FormattedExpression + + /** + * ํ‘œํ˜„์‹์„ ํŠน์ • ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ์›๋ณธ ํ‘œํ˜„์‹ + * @param sourceFormat ์›๋ณธ ํ˜•์‹ + * @param targetFormat ๋ชฉํ‘œ ํ˜•์‹ + * @return ๋ณ€ํ™˜๋œ ํ‘œํ˜„์‹ + */ + fun convert(expression: String, sourceFormat: String, targetFormat: String): FormattedExpression + + /** + * ํ‘œํ˜„์‹์„ ์ˆ˜ํ•™ ํ‘œ๊ธฐ๋ฒ•์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ณ€ํ™˜ํ•  AST ๋…ธ๋“œ + * @return ์ˆ˜ํ•™ ํ‘œ๊ธฐ๋ฒ• ํ‘œํ˜„์‹ + */ + fun toMathematicalNotation(ast: ASTNode): FormattedExpression + + /** + * ํ‘œํ˜„์‹์„ LaTeX ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ณ€ํ™˜ํ•  AST ๋…ธ๋“œ + * @return LaTeX ํ˜•์‹ ํ‘œํ˜„์‹ + */ + fun toLaTeX(ast: ASTNode): FormattedExpression + + /** + * ํ‘œํ˜„์‹์„ MathML ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ณ€ํ™˜ํ•  AST ๋…ธ๋“œ + * @return MathML ํ˜•์‹ ํ‘œํ˜„์‹ + */ + fun toMathML(ast: ASTNode): FormattedExpression + + /** + * ํ‘œํ˜„์‹์„ HTML ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ณ€ํ™˜ํ•  AST ๋…ธ๋“œ + * @return HTML ํ˜•์‹ ํ‘œํ˜„์‹ + */ + fun toHTML(ast: ASTNode): FormattedExpression + + /** + * ํ‘œํ˜„์‹์„ JSON ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ณ€ํ™˜ํ•  AST ๋…ธ๋“œ + * @return JSON ํ˜•์‹ ํ‘œํ˜„์‹ + */ + fun toJSON(ast: ASTNode): FormattedExpression + + /** + * ํ‘œํ˜„์‹์„ XML ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ณ€ํ™˜ํ•  AST ๋…ธ๋“œ + * @return XML ํ˜•์‹ ํ‘œํ˜„์‹ + */ + fun toXML(ast: ASTNode): FormattedExpression + + /** + * ํ‘œํ˜„์‹์˜ ๊ฐ€๋…์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @param expression ์›๋ณธ ํ‘œํ˜„์‹ + * @return ๊ฐ€๋…์„ฑ์ด ํ–ฅ์ƒ๋œ ํ‘œํ˜„์‹ + */ + fun beautify(expression: String): FormattedExpression + + /** + * ํ‘œํ˜„์‹์„ ์••์ถ•ํ•ฉ๋‹ˆ๋‹ค (๊ณต๋ฐฑ ์ œ๊ฑฐ ๋“ฑ). + * + * @param expression ์›๋ณธ ํ‘œํ˜„์‹ + * @return ์••์ถ•๋œ ํ‘œํ˜„์‹ + */ + fun minify(expression: String): FormattedExpression + + /** + * ํ‘œํ˜„์‹์— ๊ตฌ๋ฌธ ๊ฐ•์กฐ๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ์›๋ณธ ํ‘œํ˜„์‹ + * @param scheme ์ƒ‰์ƒ ์Šคํ‚ค๋งˆ + * @return ๊ตฌ๋ฌธ ๊ฐ•์กฐ๋œ ํ‘œํ˜„์‹ + */ + fun highlight(expression: String, scheme: String = "default"): FormattedExpression + + /** + * ํ‘œํ˜„์‹์˜ ๋ณต์žกํ•œ ๋ถ€๋ถ„์„ ์‹œ๊ฐ์ ์œผ๋กœ ๊ฐ•์กฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @return ๋ณต์žก๋„๊ฐ€ ์‹œ๊ฐํ™”๋œ ํ‘œํ˜„์‹ + */ + fun visualizeComplexity(ast: ASTNode): FormattedExpression + + /** + * ํ‘œํ˜„์‹์˜ ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @return ์‹คํ–‰ ์ˆœ์„œ๊ฐ€ ํ‘œ์‹œ๋œ ํ‘œํ˜„์‹ + */ + fun visualizeEvaluationOrder(ast: ASTNode): FormattedExpression + + /** + * ํ‘œํ˜„์‹์„ ๋‹จ๊ณ„๋ณ„๋กœ ๋ถ„ํ•ดํ•˜์—ฌ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ถ„ํ•ดํ•  AST ๋…ธ๋“œ + * @return ๋‹จ๊ณ„๋ณ„ ๋ถ„ํ•ด ๊ฒฐ๊ณผ + */ + fun breakdownSteps(ast: ASTNode): List + + /** + * ์ง€์›๋˜๋Š” ์ถœ๋ ฅ ํ˜•์‹ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ํ˜•์‹๋“ค + */ + fun getSupportedFormats(): Set + + /** + * ์ง€์›๋˜๋Š” ์ƒ‰์ƒ ์Šคํ‚ค๋งˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” ์ƒ‰์ƒ ์Šคํ‚ค๋งˆ๋“ค + */ + fun getSupportedColorSchemes(): Set + + /** + * ํ˜•์‹ํ™” ์˜ต์…˜์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param options ๊ฒ€์ฆํ•  ์˜ต์…˜ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateOptions(options: FormattingOptions): Boolean + + /** + * ํŠน์ • ํ˜•์‹์ด ์ง€์›๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param format ํ™•์ธํ•  ํ˜•์‹ + * @return ์ง€์›๋˜๋ฉด true + */ + fun supportsFormat(format: String): Boolean + + /** + * ํ‘œํ˜„์‹์˜ ์˜ˆ์ƒ ์ถœ๋ ฅ ํฌ๊ธฐ๋ฅผ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ๋ถ„์„ํ•  AST ๋…ธ๋“œ + * @param format ์ถœ๋ ฅ ํ˜•์‹ + * @return ์˜ˆ์ƒ ํฌ๊ธฐ (๋ฌธ์ž ์ˆ˜) + */ + fun estimateOutputSize(ast: ASTNode, format: String): Int + + /** + * ์ถœ๋ ฅ์ž์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map + + /** + * ์ถœ๋ ฅ์ž์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map + + /** + * ์ถœ๋ ฅ์ž๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun reset() + + /** + * ์ถœ๋ ฅ์ž๊ฐ€ ํ™œ์„ฑ ์ƒํƒœ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ™œ์„ฑ ์ƒํƒœ์ด๋ฉด true + */ + fun isActive(): Boolean + + /** + * ์บ์‹œ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param enable ์บ์‹œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + fun setCachingEnabled(enable: Boolean) + + /** + * ์ถœ๋ ฅ ํ’ˆ์งˆ ์ˆ˜์ค€์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param level ํ’ˆ์งˆ ์ˆ˜์ค€ ("low", "medium", "high") + */ + fun setQualityLevel(level: String) + + /** + * ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ์ตœ์ ํ™” ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + fun setOptimizationEnabled(enabled: Boolean) + + /** + * ๋‹ค๊ตญ์–ด ์ง€์›์„ ์œ„ํ•œ ๋กœ์ผ€์ผ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param locale ๋กœ์ผ€์ผ (์˜ˆ: "ko", "en", "ja") + */ + fun setLocale(locale: String) + + /** + * ์ˆ˜์‹ ๋ Œ๋”๋ง์„ ์œ„ํ•œ ํฐํŠธ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param fontFamily ํฐํŠธ ํŒจ๋ฐ€๋ฆฌ + * @param fontSize ํฐํŠธ ํฌ๊ธฐ + */ + fun setFont(fontFamily: String, fontSize: Int) + + /** + * ์ถœ๋ ฅ ์Šคํƒ€์ผ ํ…Œ๋งˆ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param theme ํ…Œ๋งˆ ์ด๋ฆ„ + */ + fun setTheme(theme: String) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/policies/FormattingPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/policies/FormattingPolicy.kt new file mode 100644 index 00000000..ececffac --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/policies/FormattingPolicy.kt @@ -0,0 +1,495 @@ +package hs.kr.entrydsm.domain.expresser.policies + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.expresser.entities.FormattingOptions +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ํ‘œํ˜„์‹ ํ˜•์‹ํ™” ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Policy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ‘œํ˜„์‹ ํ˜•์‹ํ™” ๊ณผ์ •์—์„œ ์ ์šฉ๋˜๋Š” + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™๊ณผ ์ •์ฑ…์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋…์„ฑ, ๋ณด์•ˆ, ์„ฑ๋Šฅ๊ณผ + * ๊ด€๋ จ๋œ ํ˜•์‹ํ™” ์ •์ฑ…์„ ์ค‘์•™ ์ง‘์ค‘์‹์œผ๋กœ ๊ด€๋ฆฌํ•˜์—ฌ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Policy( + name = "Formatting", + description = "ํ‘œํ˜„์‹ ํ˜•์‹ํ™” ๊ณผ์ •์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™๊ณผ ์ •์ฑ…์„ ๊ด€๋ฆฌ", + domain = "expresser", + scope = Scope.DOMAIN +) +class FormattingPolicy { + + companion object { + private const val MAX_OUTPUT_LENGTH = 100000 + private const val MAX_DEPTH_FOR_PRETTY_PRINT = 50 + private const val MAX_LINE_LENGTH = 120 + private const val MAX_NESTING_LEVELS = 20 + private const val MIN_FONT_SIZE = 8 + private const val MAX_FONT_SIZE = 72 + + // ํ—ˆ์šฉ๋œ ์ถœ๋ ฅ ํ˜•์‹๋“ค + private val ALLOWED_FORMATS = setOf( + "infix", "prefix", "postfix", "latex", "mathml", "html", + "json", "xml", "text", "unicode", "ascii" + ) + + // ํ—ˆ์šฉ๋œ ์ƒ‰์ƒ ์Šคํ‚ค๋งˆ๋“ค + private val ALLOWED_COLOR_SCHEMES = setOf( + "default", "dark", "light", "high-contrast", "colorblind", + "solarized", "monokai", "github", "idea" + ) + + // ํ—ˆ์šฉ๋œ ํ…Œ๋งˆ๋“ค + private val ALLOWED_THEMES = setOf( + "classic", "modern", "minimal", "academic", "presentation", + "print", "web", "mobile" + ) + + // ์•ˆ์ „ํ•œ HTML ํƒœ๊ทธ๋“ค + private val SAFE_HTML_TAGS = setOf( + "span", "div", "sub", "sup", "em", "strong", "i", "b", + "math", "mi", "mn", "mo", "mrow", "mfrac", "msqrt", "mroot" + ) + + // ๊ธˆ์ง€๋œ ๋ฌธ์ž๋“ค (๋ณด์•ˆ) + private val FORBIDDEN_CHARACTERS = setOf( + '<', '>', '"', '\'', '&', '\u0000', '\u001F' + ).filter { it.code <= 31 || it.code == 127 }.toSet() + } + + private val formatMetrics = mutableMapOf() + + /** + * ํ˜•์‹ํ™” ์˜ต์…˜์ด ํ—ˆ์šฉ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param options ๊ฒ€์ฆํ•  ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ํ—ˆ์šฉ๋˜๋ฉด true + */ + fun isFormattingAllowed(options: FormattingOptions): Boolean { + return try { + validateFormat(options.style.toString()) && + validateColorScheme(options.toString()) && + validateTheme(options.toString()) && + validateSafety(options) && + validatePerformance(options) + } catch (e: Exception) { + false + } + } + + /** + * ์ถœ๋ ฅ ๊ธธ์ด๊ฐ€ ํ—ˆ์šฉ ๋ฒ”์œ„ ๋‚ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param content ํ™•์ธํ•  ๋‚ด์šฉ + * @return ํ—ˆ์šฉ ๋ฒ”์œ„ ๋‚ด๋ฉด true + */ + fun isOutputLengthAcceptable(content: String): Boolean { + return content.length <= MAX_OUTPUT_LENGTH + } + + /** + * ํ˜•์‹ํ™” ๋ณต์žก๋„๊ฐ€ ํ—ˆ์šฉ ๋ฒ”์œ„ ๋‚ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ํ™•์ธํ•  AST ๋…ธ๋“œ + * @return ํ—ˆ์šฉ ๋ฒ”์œ„ ๋‚ด๋ฉด true + */ + fun isComplexityAcceptable(ast: ASTNode): Boolean { + val depth = calculateASTDepth(ast) + val nodeCount = countASTNodes(ast) + + return depth <= MAX_DEPTH_FOR_PRETTY_PRINT && + nodeCount <= 10000 // ๋…ธ๋“œ ๊ฐœ์ˆ˜ ์ œํ•œ + } + + /** + * ์ถœ๋ ฅ ๋‚ด์šฉ์ด ์•ˆ์ „ํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param content ํ™•์ธํ•  ๋‚ด์šฉ + * @param format ์ถœ๋ ฅ ํ˜•์‹ + * @return ์•ˆ์ „ํ•˜๋ฉด true + */ + fun isContentSafe(content: String, format: String): Boolean { + return when (format.lowercase()) { + "html", "mathml", "xml" -> isHTMLSafe(content) + "latex" -> isLatexSafe(content) + "json" -> isJSONSafe(content) + else -> isGenerallySafe(content) + } + } + + /** + * ํฐํŠธ ์„ค์ •์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param fontFamily ํฐํŠธ ํŒจ๋ฐ€๋ฆฌ + * @param fontSize ํฐํŠธ ํฌ๊ธฐ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isFontSettingValid(fontFamily: String, fontSize: Int): Boolean { + return fontFamily.isNotBlank() && + fontFamily.length <= 100 && + fontSize in MIN_FONT_SIZE..MAX_FONT_SIZE && + !containsDangerousContent(fontFamily) + } + + /** + * ์ƒ‰์ƒ ๊ฐ’์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param color ์ƒ‰์ƒ ๊ฐ’ (์˜ˆ: "#FF0000", "red", "rgb(255,0,0)") + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isColorValid(color: String): Boolean { + return when { + color.startsWith("#") -> isHexColorValid(color) + color.startsWith("rgb") -> isRGBColorValid(color) + color.startsWith("hsl") -> isHSLColorValid(color) + else -> isNamedColorValid(color) + } + } + + /** + * ์ค„ ๊ธธ์ด ์ œํ•œ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param content ๋‚ด์šฉ + * @param maxLength ์ตœ๋Œ€ ์ค„ ๊ธธ์ด + * @return ์ œํ•œ์ด ์ ์šฉ๋œ ๋‚ด์šฉ + */ + fun applyLineLengthLimit(content: String, maxLength: Int = MAX_LINE_LENGTH): String { + val effectiveMaxLength = maxLength.coerceAtMost(MAX_LINE_LENGTH) + + return content.lines().joinToString("\n") { line -> + if (line.length <= effectiveMaxLength) { + line + } else { + // ์ ์ ˆํ•œ ์œ„์น˜์—์„œ ์ค„ ๋ฐ”๊ฟˆ + breakLongLine(line, effectiveMaxLength) + } + } + } + + /** + * ์ค‘์ฒฉ ๊นŠ์ด ์ œํ•œ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast AST ๋…ธ๋“œ + * @return ์ œํ•œ ์ ์šฉ ์—ฌ๋ถ€ + */ + fun applyNestingLimit(ast: ASTNode): Boolean { + val depth = calculateASTDepth(ast) + return depth <= MAX_NESTING_LEVELS + } + + /** + * ๋ณด์•ˆ ํ•„ํ„ฐ๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param content ํ•„ํ„ฐ๋งํ•  ๋‚ด์šฉ + * @param format ์ถœ๋ ฅ ํ˜•์‹ + * @return ํ•„ํ„ฐ๋ง๋œ ๋‚ด์šฉ + */ + fun applySecurityFilter(content: String, format: String): String { + var filtered = content + + // ๊ธˆ์ง€๋œ ๋ฌธ์ž ์ œ๊ฑฐ + FORBIDDEN_CHARACTERS.forEach { char -> + filtered = filtered.replace(char.toString(), "") + } + + // ํ˜•์‹๋ณ„ ํŠน์ˆ˜ ํ•„ํ„ฐ๋ง + filtered = when (format.lowercase()) { + "html", "mathml", "xml" -> escapeHTML(filtered) + "latex" -> escapeLatex(filtered) + "json" -> escapeJSON(filtered) + else -> filtered + } + + return filtered + } + + /** + * ์„ฑ๋Šฅ ์ตœ์ ํ™”๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param content ์ตœ์ ํ™”ํ•  ๋‚ด์šฉ + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ์ตœ์ ํ™”๋œ ๋‚ด์šฉ + */ + fun applyPerformanceOptimization(content: String, options: FormattingOptions): String { + var optimized = content + + // ๋ถˆํ•„์š”ํ•œ ๊ณต๋ฐฑ ์ œ๊ฑฐ (์„ค์ •์— ๋”ฐ๋ผ) + if (options.toString().contains("minify")) { + optimized = optimized.replace(Regex("\\s+"), " ").trim() + } + + // ์ค‘๋ณต ์ œ๊ฑฐ + optimized = removeDuplicateSpaces(optimized) + + // ๊ธธ์ด ์ œํ•œ ์ ์šฉ + if (optimized.length > MAX_OUTPUT_LENGTH) { + optimized = optimized.take(MAX_OUTPUT_LENGTH - 3) + "..." + } + + return optimized + } + + /** + * ์ ‘๊ทผ์„ฑ ๊ฐ€์ด๋“œ๋ผ์ธ์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param content ๋‚ด์šฉ + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ์ ‘๊ทผ์„ฑ์ด ๊ฐœ์„ ๋œ ๋‚ด์šฉ + */ + fun applyAccessibilityGuidelines(content: String, options: FormattingOptions): String { + var accessible = content + + // ์ƒ‰์ƒ ๋Œ€๋น„ ๊ฐœ์„  + if (options.toString().contains("high-contrast")) { + accessible = applyHighContrastColors(accessible) + } + + // ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์ง€์› ๊ฐœ์„  + accessible = addAriaLabels(accessible) + + // ๋Œ€์ฒด ํ…์ŠคํŠธ ์ถ”๊ฐ€ + accessible = addAltText(accessible) + + return accessible + } + + /** + * ํ˜•์‹ํ™” ๋ฉ”ํŠธ๋ฆญ์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. + * + * @param format ํ˜•์‹ + * @param processingTime ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ + * @param outputSize ์ถœ๋ ฅ ํฌ๊ธฐ + */ + fun updateMetrics(format: String, processingTime: Long, outputSize: Int) { + val metrics = formatMetrics.getOrPut(format) { FormatMetrics() } + metrics.update(processingTime, outputSize) + } + + // Private helper methods + + private fun validateFormat(format: String): Boolean { + return format.lowercase() in ALLOWED_FORMATS + } + + private fun validateColorScheme(scheme: String): Boolean { + return scheme.lowercase() in ALLOWED_COLOR_SCHEMES + } + + private fun validateTheme(theme: String): Boolean { + return theme.lowercase() in ALLOWED_THEMES + } + + private fun validateSafety(options: FormattingOptions): Boolean { + val optionsStr = options.toString() + return !containsDangerousContent(optionsStr) + } + + private fun validatePerformance(options: FormattingOptions): Boolean { + // ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ๋Š” ์˜ต์…˜๋“ค ๊ฒ€์‚ฌ + return true // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ + } + + private fun calculateASTDepth(ast: ASTNode): Int { + return if (ast.getChildren().isEmpty()) { + 1 + } else { + 1 + (ast.getChildren().maxOfOrNull { calculateASTDepth(it) } ?: 0) + } + } + + private fun countASTNodes(ast: ASTNode): Int { + return 1 + ast.getChildren().sumOf { countASTNodes(it) } + } + + private fun isHTMLSafe(content: String): Boolean { + // HTML ํƒœ๊ทธ ์•ˆ์ „์„ฑ ๊ฒ€์‚ฌ + val tagPattern = Regex("<(/?)([a-zA-Z][a-zA-Z0-9]*)") + val matches = tagPattern.findAll(content) + + return matches.all { match -> + val tagName = match.groupValues[2].lowercase() + tagName in SAFE_HTML_TAGS + } + } + + private fun isLatexSafe(content: String): Boolean { + // LaTeX ๋ช…๋ น์–ด ์•ˆ์ „์„ฑ ๊ฒ€์‚ฌ + val dangerousCommands = setOf("\\input", "\\include", "\\write", "\\openout", "\\read") + return dangerousCommands.none { content.contains(it) } + } + + private fun isJSONSafe(content: String): Boolean { + // JSON ์•ˆ์ „์„ฑ ๊ฒ€์‚ฌ + try { + // ๊ธฐ๋ณธ์ ์ธ JSON ํ˜•์‹ ๊ฒ€์‚ฌ + return !content.contains("") && !content.contains("javascript:") + } catch (e: Exception) { + return false + } + } + + private fun isGenerallySafe(content: String): Boolean { + return !containsDangerousContent(content) + } + + private fun containsDangerousContent(content: String): Boolean { + val dangerousPatterns = listOf( + Regex(" ", ", ") + + for (breakPoint in breakPoints) { + val index = line.lastIndexOf(breakPoint, maxLength - breakPoint.length) + if (index > 0) { + return line.substring(0, index + breakPoint.length) + "\n " + + breakLongLine(line.substring(index + breakPoint.length), maxLength) + } + } + + // ์ ์ ˆํ•œ ๊ตฌ๋ถ„์ ์ด ์—†์œผ๋ฉด ๊ฐ•์ œ๋กœ ์ž๋ฆ„ + return line.substring(0, maxLength - 3) + "...\n " + + breakLongLine(line.substring(maxLength - 3), maxLength) + } + + private fun removeDuplicateSpaces(content: String): String { + return content.replace(Regex("\\s+"), " ") + } + + private fun escapeHTML(content: String): String { + return content.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace("\"", """) + .replace("'", "'") + } + + private fun escapeLatex(content: String): String { + return content.replace("\\", "\\textbackslash{}") + .replace("{", "\\{") + .replace("}", "\\}") + .replace("$", "\\$") + .replace("&", "\\&") + .replace("%", "\\%") + .replace("#", "\\#") + .replace("^", "\\textasciicircum{}") + .replace("_", "\\_") + .replace("~", "\\textasciitilde{}") + } + + private fun escapeJSON(content: String): String { + return content.replace("\\", "\\\\") + .replace("\"", "\\\"") + .replace("/", "\\/") + .replace("\b", "\\b") + .replace("\n", "\\n") + .replace("\r", "\\r") + .replace("\t", "\\t") + } + + private fun applyHighContrastColors(content: String): String { + // ๊ณ ๋Œ€๋น„ ์ƒ‰์ƒ ์ ์šฉ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return content.replace("color: gray", "color: black") + .replace("color: #888", "color: #000") + } + + private fun addAriaLabels(content: String): String { + // ARIA ๋ผ๋ฒจ ์ถ”๊ฐ€ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return content.replace(" 0) totalProcessingTime.toDouble() / totalRequests else 0.0 + + fun getAverageOutputSize(): Double = + if (totalRequests > 0) totalOutputSize.toDouble() / totalRequests else 0.0 + } + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxOutputLength" to MAX_OUTPUT_LENGTH, + "maxDepthForPrettyPrint" to MAX_DEPTH_FOR_PRETTY_PRINT, + "maxLineLength" to MAX_LINE_LENGTH, + "maxNestingLevels" to MAX_NESTING_LEVELS, + "minFontSize" to MIN_FONT_SIZE, + "maxFontSize" to MAX_FONT_SIZE, + "allowedFormats" to ALLOWED_FORMATS.size, + "allowedColorSchemes" to ALLOWED_COLOR_SCHEMES.size, + "allowedThemes" to ALLOWED_THEMES.size, + "safeHtmlTags" to SAFE_HTML_TAGS.size + ) + + /** + * ์ •์ฑ…์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "policyName" to "FormattingPolicy", + "activeFormats" to formatMetrics.size, + "securityRules" to listOf("html_safety", "latex_safety", "json_safety", "content_filtering"), + "performanceRules" to listOf("length_limits", "nesting_limits", "line_breaks"), + "accessibilityFeatures" to listOf("high_contrast", "aria_labels", "alt_text") + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/services/ExpresserService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/services/ExpresserService.kt new file mode 100644 index 00000000..60b992d1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/services/ExpresserService.kt @@ -0,0 +1,624 @@ +package hs.kr.entrydsm.domain.expresser.services + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.expresser.aggregates.ExpressionFormatter +import hs.kr.entrydsm.domain.expresser.aggregates.ExpressionReporter +import hs.kr.entrydsm.domain.expresser.entities.FormattingOptions +import hs.kr.entrydsm.domain.expresser.entities.FormattingStyle +import hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException +import hs.kr.entrydsm.domain.expresser.factories.ExpresserFactory +import hs.kr.entrydsm.domain.expresser.interfaces.ExpresserContract +import hs.kr.entrydsm.domain.expresser.policies.FormattingPolicy +import hs.kr.entrydsm.domain.expresser.specifications.FormattingQualitySpec +import hs.kr.entrydsm.domain.expresser.values.FormattedExpression +import hs.kr.entrydsm.domain.lexer.aggregates.LexerAggregate +import hs.kr.entrydsm.domain.parser.aggregates.ParsingContextAggregate +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.configuration.ExpresserConfiguration +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider +import java.time.Instant + +/** + * ํ‘œํ˜„์‹ ํ˜•์‹ํ™”์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๋„๋ฉ”์ธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Domain Service ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ‘œํ˜„์‹ ํ˜•์‹ํ™”์™€ ์ถœ๋ ฅ ์ƒ์„ฑ์˜ + * ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ์„ ์กฐ์œจํ•ฉ๋‹ˆ๋‹ค. ์ •์ฑ…๊ณผ ๋ช…์„ธ๋ฅผ ์ ์šฉํ•˜์—ฌ ์•ˆ์ „ํ•˜๊ณ  + * ํ’ˆ์งˆ ๋†’์€ ํ˜•์‹ํ™”๋ฅผ ๋ณด์žฅํ•˜๋ฉฐ, ๋‹ค์–‘ํ•œ ํ˜•์‹๊ณผ ์Šคํƒ€์ผ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Service( + name = "ExpresserService", + type = hs.kr.entrydsm.global.annotation.service.type.ServiceType.DOMAIN_SERVICE +) +class ExpresserService( + private val lexer: LexerAggregate, + private val parser: ParsingContextAggregate, + private val factory: ExpresserFactory, + private val policy: FormattingPolicy, + private val qualitySpec: FormattingQualitySpec, + private val configurationProvider: ConfigurationProvider +) : ExpresserContract { + + companion object { + /** + * ํฌ๋งท๋ณ„ ํŒฉํ† ๋ฆฌ ๋ฉ”์„œ๋“œ ๋งคํ•‘ + */ + private val FORMAT_FACTORY_MAPPINGS = mapOf( + "mathematical" to { factory: ExpresserFactory -> factory.createBasicFormatter() }, + "latex" to { factory: ExpresserFactory -> factory.createLaTeXFormatter() }, + "mathml" to { factory: ExpresserFactory -> factory.createMathMLFormatter() }, + "html" to { factory: ExpresserFactory -> factory.createHTMLFormatter() }, + "unicode" to { factory: ExpresserFactory -> factory.createUnicodeFormatter() }, + "ascii" to { factory: ExpresserFactory -> factory.createASCIIFormatter() } + ) + + /** + * ์ถœ๋ ฅ ํฌ๊ธฐ ์ถ”์ • ๋ฐฐ์œจ + */ + private val OUTPUT_SIZE_MULTIPLIERS = mapOf( + "latex" to 1.5, + "mathml" to 2.0, + "html" to 1.8, + "xml" to 2.2, + "json" to 1.3 + ) + + /** + * ์ง€์›๋˜๋Š” ์ถœ๋ ฅ ํ˜•์‹๋“ค + */ + private val SUPPORTED_FORMATS = setOf( + "mathematical", "latex", "mathml", "html", "json", "xml", "unicode", "ascii", "text" + ) + + /** + * ์ง€์›๋˜๋Š” ์ƒ‰์ƒ ์Šคํ‚ค๋งˆ๋“ค + */ + private val SUPPORTED_COLOR_SCHEMES = setOf( + "default", "dark", "light", "high-contrast", "colorblind" + ) + + /** + * ์บ์‹œ ๊ด€๋ จ ์ƒ์ˆ˜๋“ค + */ + private const val CACHE_VALIDITY_MS = 3600000L // 1์‹œ๊ฐ„ + private const val MAX_CACHE_SIZE = 1000 + + /** + * ๋‹จ๊ณ„๋ณ„ ๋ถ„ํ•ด ํ…œํ”Œ๋ฆฟ + */ + private val BREAKDOWN_STEPS = listOf( + "Step 1: Parse", + "Step 2: Format" + ) + + /** + * ๊ตฌ๋ฌธ ๊ฐ•์กฐ CSS ํด๋ž˜์Šค ๋งคํ•‘ + */ + private val SYNTAX_HIGHLIGHT_CLASSES = mapOf( + "dark" to mapOf("number" to "number-dark"), + "light" to mapOf("number" to "number-light") + ) + } + + private val config: ExpresserConfiguration + get() = configurationProvider.getExpresserConfiguration() + + private val formattingCache = mutableMapOf() + private val performanceMetrics = PerformanceMetrics() + + /** + * AST๋ฅผ ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun format(ast: ASTNode): FormattedExpression { + return format(ast, FormattingOptions.default()) + } + + /** + * AST๋ฅผ ํŠน์ • ์˜ต์…˜์œผ๋กœ ํ˜•์‹ํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun format(ast: ASTNode, options: FormattingOptions): FormattedExpression { + val startTime = System.currentTimeMillis() + + try { + performanceMetrics.incrementTotalRequests() + + // ๋‹จ๊ณ„๋ณ„ ์ฒ˜๋ฆฌ + validateFormattingRequest(ast, options) + val cacheResult = tryGetCachedResult(ast, options) + if (cacheResult != null) return cacheResult + + val rawFormatted = executeFormatting(ast, options) + val validatedFormatted = validateFormattingQuality(rawFormatted, options) + val secureFormatted = applySecurityFiltering(validatedFormatted) + val finalResult = cacheFinalResult(ast, options, secureFormatted) + + updateMetricsAndFinalize(startTime, finalResult) + return finalResult + + } catch (e: ExpresserException) { + performanceMetrics.incrementFailures() + throw e + } catch (e: Exception) { + performanceMetrics.incrementFailures() + throw ExpresserException.formattingError("formatting_error", e.message ?: "Unknown error", e) + } + } + + /** + * ํ‘œํ˜„์‹ ๋ฌธ์ž์—ด์„ ์žฌํ˜•์‹ํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun reformat(expression: String): FormattedExpression { + return reformat(expression, FormattingOptions.default()) + } + + /** + * ํ‘œํ˜„์‹ ๋ฌธ์ž์—ด์„ ํŠน์ • ์˜ต์…˜์œผ๋กœ ์žฌํ˜•์‹ํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun reformat(expression: String, options: FormattingOptions): FormattedExpression { + try { + // 1. ๋ ‰์‹ฑ + val lexingResult = lexer.tokenize(expression) + if (!lexingResult.isSuccess) { + throw ExpresserException.formattingError("lexing_failed", lexingResult.error?.message ?: "๋ ‰์‹ฑ ์‹คํŒจ: ํ† ํฐํ™” ์˜ค๋ฅ˜") + } + + // 2. ํŒŒ์‹ฑ + val parsingResult = parser.parse(lexingResult.tokens) + if (!parsingResult.isSuccess) { + throw ExpresserException.formattingError("parsing_failed", parsingResult.error?.message ?: "ํŒŒ์‹ฑ ์‹คํŒจ") + } + val ast = parsingResult.ast!! + + // 3. ํ˜•์‹ํ™” + return format(ast, options) + + } catch (e: ExpresserException) { + throw e + } catch (e: Exception) { + throw ExpresserException.formattingError("reformat_error", e.message ?: "Unknown error", e) + } + } + + /** + * AST๋ฅผ ํŠน์ • ํ˜•์‹์œผ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + */ + override fun express(ast: ASTNode, format: String): FormattedExpression { + val factoryMethod = FORMAT_FACTORY_MAPPINGS[format.lowercase()] + ?: throw ExpresserException.unsupportedFormat(format) + + val formatter = factoryMethod(factory) + return formatter.format(ast) + } + + /** + * ํ‘œํ˜„์‹์„ ํŠน์ • ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun convert(expression: String, sourceFormat: String, targetFormat: String): FormattedExpression { + // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ - ์‹ค์ œ๋กœ๋Š” ๋” ์ •๊ตํ•œ ๋ณ€ํ™˜ ๋กœ์ง ํ•„์š” + val reformatted = reformat(expression) + val lexingResult = lexer.tokenize(expression) + if (!lexingResult.isSuccess) { + throw ExpresserException.formattingError("lexing_failed", lexingResult.error?.message ?: "๋ ‰์‹ฑ ์‹คํŒจ") + } + val parsingResult = parser.parse(lexingResult.tokens) + if (!parsingResult.isSuccess) { + throw ExpresserException.formattingError("parsing_failed", parsingResult.error?.message ?: "ํŒŒ์‹ฑ ์‹คํŒจ") + } + return express(parsingResult.ast!!, targetFormat) + } + + /** + * ํ‘œํ˜„์‹์„ ์ˆ˜ํ•™ ํ‘œ๊ธฐ๋ฒ•์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun toMathematicalNotation(ast: ASTNode): FormattedExpression { + return express(ast, "mathematical") + } + + /** + * ํ‘œํ˜„์‹์„ LaTeX ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun toLaTeX(ast: ASTNode): FormattedExpression { + return express(ast, "latex") + } + + /** + * ํ‘œํ˜„์‹์„ MathML ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun toMathML(ast: ASTNode): FormattedExpression { + return express(ast, "mathml") + } + + /** + * ํ‘œํ˜„์‹์„ HTML ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun toHTML(ast: ASTNode): FormattedExpression { + return express(ast, "html") + } + + /** + * ํ‘œํ˜„์‹์„ JSON ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun toJSON(ast: ASTNode): FormattedExpression { + val formatted = format(ast) + return factory.createFormattedExpression( + content = formatted.toJson(), + format = "json", + metadata = mapOf("originalFormat" to formatted.style.name) + ) + } + + /** + * ํ‘œํ˜„์‹์„ XML ํ˜•์‹์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun toXML(ast: ASTNode): FormattedExpression { + val formatted = format(ast) + return factory.createFormattedExpression( + content = formatted.toXml(), + format = "xml", + metadata = mapOf("originalFormat" to formatted.style.name) + ) + } + + /** + * ํ‘œํ˜„์‹์˜ ๊ฐ€๋…์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + */ + override fun beautify(expression: String): FormattedExpression { + val options = FormattingOptions.forStyle(FormattingStyle.MATHEMATICAL) + .withSpaces(true) + .withOperatorSpacing(true) + return reformat(expression, options) + } + + /** + * ํ‘œํ˜„์‹์„ ์••์ถ•ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun minify(expression: String): FormattedExpression { + val options = FormattingOptions.compact() + val formatted = reformat(expression, options) + val optimized = policy.applyPerformanceOptimization(formatted.expression, options) + return formatted.copy(expression = optimized) + } + + /** + * ํ‘œํ˜„์‹์— ๊ตฌ๋ฌธ ๊ฐ•์กฐ๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + */ + override fun highlight(expression: String, scheme: String): FormattedExpression { + val formatted = reformat(expression) + val highlighted = when (scheme) { + "dark" -> applyDarkSyntaxHighlight(formatted.expression) + "light" -> applyLightSyntaxHighlight(formatted.expression) + else -> formatted.expression + } + return formatted.copy(expression = highlighted) + } + + /** + * ํ‘œํ˜„์‹์˜ ๋ณต์žกํ•œ ๋ถ€๋ถ„์„ ์‹œ๊ฐ์ ์œผ๋กœ ๊ฐ•์กฐํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visualizeComplexity(ast: ASTNode): FormattedExpression { + val formatted = format(ast) + val complexity = formatted.calculateComplexity() + return factory.createFormattedExpression( + content = formatted.expression, + format = "complexity-visualized", + metadata = mapOf( + "complexity" to complexity, + "visualization" to "enabled" + ) + ) + } + + /** + * ํ‘œํ˜„์‹์˜ ์‹คํ–‰ ์ˆœ์„œ๋ฅผ ์‹œ๊ฐ์ ์œผ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. + */ + override fun visualizeEvaluationOrder(ast: ASTNode): FormattedExpression { + val formatted = format(ast) + // ๊ฐ„๋‹จํ•œ ์‹คํ–‰ ์ˆœ์„œ ํ‘œ์‹œ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + val withOrder = addEvaluationOrder(formatted.expression) + return formatted.copy(expression = withOrder) + } + + /** + * ํ‘œํ˜„์‹์„ ๋‹จ๊ณ„๋ณ„๋กœ ๋ถ„ํ•ดํ•˜์—ฌ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. + */ + override fun breakdownSteps(ast: ASTNode): List { + val mainFormatted = format(ast) + return BREAKDOWN_STEPS.map { step -> + factory.createFormattedExpression(step, "step") + } + mainFormatted + } + + /** + * ์ง€์›๋˜๋Š” ์ถœ๋ ฅ ํ˜•์‹ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun getSupportedFormats(): Set { + return SUPPORTED_FORMATS + } + + /** + * ์ง€์›๋˜๋Š” ์ƒ‰์ƒ ์Šคํ‚ค๋งˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun getSupportedColorSchemes(): Set { + return SUPPORTED_COLOR_SCHEMES + } + + /** + * ํ˜•์‹ํ™” ์˜ต์…˜์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + override fun validateOptions(options: FormattingOptions): Boolean { + return policy.isFormattingAllowed(options) && options.isValid() + } + + /** + * ํŠน์ • ํ˜•์‹์ด ์ง€์›๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + override fun supportsFormat(format: String): Boolean { + return getSupportedFormats().contains(format.lowercase()) + } + + /** + * ํ‘œํ˜„์‹์˜ ์˜ˆ์ƒ ์ถœ๋ ฅ ํฌ๊ธฐ๋ฅผ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun estimateOutputSize(ast: ASTNode, format: String): Int { + val baseSize = ast.toString().length + val multiplier = OUTPUT_SIZE_MULTIPLIERS[format.lowercase()] ?: 1.0 + return (baseSize * multiplier).toInt() + } + + /** + * ์„œ๋น„์Šค์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun getConfiguration(): Map { + return mapOf( + "serviceName" to "ExpresserService", + "defaultTimeoutMs" to config.defaultTimeoutMs, + "maxRetries" to config.maxRetries, + "cacheEnabled" to config.cachingEnabled, + "maxCacheSize" to config.maxCacheSize, + "enableQualityCheck" to config.enableQualityCheck, + "enableSecurityFilter" to config.enableSecurityFilter, + "supportedFormats" to config.supportedFormats, + "supportedColorSchemes" to getSupportedColorSchemes() + ) + } + + /** + * ์„œ๋น„์Šค์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun getStatistics(): Map { + val metrics = performanceMetrics.getMetrics() + return metrics + mapOf( + "currentCacheSize" to formattingCache.size, + "policyStatistics" to policy.getStatistics(), + "qualitySpecStatistics" to qualitySpec.getStatistics() + ) + } + + /** + * ์„œ๋น„์Šค๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun reset() { + formattingCache.clear() + performanceMetrics.reset() + } + + /** + * ์„œ๋น„์Šค๊ฐ€ ํ™œ์„ฑ ์ƒํƒœ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + override fun isActive(): Boolean { + return true // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ + } + + /** + * ์บ์‹œ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + override fun setCachingEnabled(enable: Boolean) { + if (!enable) { + formattingCache.clear() + } + } + + /** + * ์ถœ๋ ฅ ํ’ˆ์งˆ ์ˆ˜์ค€์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun setQualityLevel(level: String) { + // ํ’ˆ์งˆ ์ˆ˜์ค€์— ๋”ฐ๋ฅธ ์„ค์ • ์กฐ์ • + } + + /** + * ์„ฑ๋Šฅ ์ตœ์ ํ™” ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun setOptimizationEnabled(enabled: Boolean) { + // ์ตœ์ ํ™” ๋ชจ๋“œ ์„ค์ • + } + + /** + * ๋‹ค๊ตญ์–ด ์ง€์›์„ ์œ„ํ•œ ๋กœ์ผ€์ผ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun setLocale(locale: String) { + // ๋กœ์ผ€์ผ ์„ค์ • + } + + /** + * ์ˆ˜์‹ ๋ Œ๋”๋ง์„ ์œ„ํ•œ ํฐํŠธ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun setFont(fontFamily: String, fontSize: Int) { + // ํฐํŠธ ์„ค์ • + } + + /** + * ์ถœ๋ ฅ ์Šคํƒ€์ผ ํ…Œ๋งˆ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun setTheme(theme: String) { + // ํ…Œ๋งˆ ์„ค์ • + } + + /** + * ํ˜•์‹ํ™” ์š”์ฒญ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateFormattingRequest(ast: ASTNode, options: FormattingOptions) { + // 1. ์˜ต์…˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + if (!policy.isFormattingAllowed(options)) { + throw ExpresserException.invalidFormatOption(options.toString()) + } + + // 2. ๋ณต์žก๋„ ๊ฒ€์ฆ + if (!policy.isComplexityAcceptable(ast)) { + throw ExpresserException.formattingError("complexity_check_failed", "๋ณต์žก๋„ ์ดˆ๊ณผ") + } + } + + /** + * ์บ์‹œ๋œ ๊ฒฐ๊ณผ๋ฅผ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun tryGetCachedResult(ast: ASTNode, options: FormattingOptions): FormattedExpression? { + val cacheKey = generateCacheKey(ast, options) + val cachedResult = getCachedFormatting(cacheKey) + if (cachedResult != null) { + performanceMetrics.incrementCacheHits() + return cachedResult.toFormattedExpression() + } + return null + } + + /** + * ์‹ค์ œ ํ˜•์‹ํ™”๋ฅผ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun executeFormatting(ast: ASTNode, options: FormattingOptions): FormattedExpression { + val formatter = factory.createCustomFormatter(options) + return formatter.format(ast) + } + + /** + * ํ˜•์‹ํ™” ํ’ˆ์งˆ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateFormattingQuality(formatted: FormattedExpression, options: FormattingOptions): FormattedExpression { + if (!qualitySpec.isSatisfiedBy(formatted, options)) { + val issues = qualitySpec.identifyQualityIssues(formatted, options) + val criticalIssues = issues.filter { it.severity == FormattingQualitySpec.QualityIssue.Severity.HIGH } + if (criticalIssues.isNotEmpty()) { + throw ExpresserException.formattingError( + "quality_check_failed", + "ํ’ˆ์งˆ ๊ธฐ์ค€ ๋ฏธ๋‹ฌ: ${criticalIssues.joinToString { it.message }}" + ) + } + } + return formatted + } + + /** + * ๋ณด์•ˆ ํ•„ํ„ฐ๋ง์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + */ + private fun applySecurityFiltering(formatted: FormattedExpression): FormattedExpression { + val safeContent = policy.applySecurityFilter(formatted.expression, "text") + return formatted.copy(expression = safeContent) + } + + /** + * ์ตœ์ข… ๊ฒฐ๊ณผ๋ฅผ ์บ์‹ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private fun cacheFinalResult(ast: ASTNode, options: FormattingOptions, formatted: FormattedExpression): FormattedExpression { + val cacheKey = generateCacheKey(ast, options) + cacheFormatting(cacheKey, formatted) + return formatted + } + + /** + * ๋ฉ”ํŠธ๋ฆญ์„ ์—…๋ฐ์ดํŠธํ•˜๊ณ  ๋งˆ๋ฌด๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + private fun updateMetricsAndFinalize(startTime: Long, formatted: FormattedExpression) { + val executionTime = System.currentTimeMillis() - startTime + policy.updateMetrics("format", executionTime, formatted.expression.length) + performanceMetrics.updateExecutionTime(executionTime) + } + + + private fun generateCacheKey(ast: ASTNode, options: FormattingOptions): String { + return "${ast.toString().hashCode()}_${options.hashCode()}" + } + + private fun getCachedFormatting(key: String): CachedFormatting? { + return formattingCache[key]?.takeIf { + System.currentTimeMillis() - it.timestamp < CACHE_VALIDITY_MS + } + } + + private fun cacheFormatting(key: String, formatted: FormattedExpression) { + if (formattingCache.size < MAX_CACHE_SIZE) { + formattingCache[key] = CachedFormatting( + formatted = formatted, + timestamp = System.currentTimeMillis() + ) + } + } + + private fun applyDarkSyntaxHighlight(content: String): String { + val numberClass = SYNTAX_HIGHLIGHT_CLASSES["dark"]?.get("number") ?: "number-dark" + return content.replace(Regex("\\d+")) { "${it.value}" } + } + + private fun applyLightSyntaxHighlight(content: String): String { + val numberClass = SYNTAX_HIGHLIGHT_CLASSES["light"]?.get("number") ?: "number-light" + return content.replace(Regex("\\d+")) { "${it.value}" } + } + + private fun addEvaluationOrder(content: String): String { + // ๊ฐ„๋‹จํ•œ ์‹คํ–‰ ์ˆœ์„œ ํ‘œ์‹œ + return "1โ†’($content)" + } + + /** + * ์บ์‹œ๋œ ํ˜•์‹ํ™” ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + private data class CachedFormatting( + val formatted: FormattedExpression, + val timestamp: Long + ) { + fun toFormattedExpression(): FormattedExpression { + return formatted.copy(createdAt = System.currentTimeMillis()) + } + } + + /** + * ์„ฑ๋Šฅ ๋ฉ”ํŠธ๋ฆญ์„ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + private class PerformanceMetrics { + private var totalRequests = 0L + private var totalFailures = 0L + private var totalCacheHits = 0L + private var totalExecutionTime = 0L + private var requestCount = 0L + + fun incrementTotalRequests() = synchronized(this) { totalRequests++ } + fun incrementFailures() = synchronized(this) { totalFailures++ } + fun incrementCacheHits() = synchronized(this) { totalCacheHits++ } + + fun updateExecutionTime(time: Long) = synchronized(this) { + totalExecutionTime += time + requestCount++ + } + + fun reset() = synchronized(this) { + totalRequests = 0 + totalFailures = 0 + totalCacheHits = 0 + totalExecutionTime = 0 + requestCount = 0 + } + + fun getMetrics(): Map = synchronized(this) { + mapOf( + "totalRequests" to totalRequests, + "totalFailures" to totalFailures, + "totalCacheHits" to totalCacheHits, + "averageExecutionTime" to if (requestCount > 0) totalExecutionTime.toDouble() / requestCount else 0.0, + "successRate" to if (totalRequests > 0) ((totalRequests - totalFailures).toDouble() / totalRequests) else 0.0, + "cacheHitRate" to if (totalRequests > 0) (totalCacheHits.toDouble() / totalRequests) else 0.0 + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/specifications/FormattingQualitySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/specifications/FormattingQualitySpec.kt new file mode 100644 index 00000000..684edc06 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/specifications/FormattingQualitySpec.kt @@ -0,0 +1,555 @@ +package hs.kr.entrydsm.domain.expresser.specifications + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.expresser.entities.FormattingOptions +import hs.kr.entrydsm.domain.expresser.values.FormattedExpression +import hs.kr.entrydsm.global.annotation.specification.Specification + +/** + * ํ˜•์‹ํ™” ํ’ˆ์งˆ ๊ฒ€์ฆ ๋ช…์„ธ๋ฅผ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Specification ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ‘œํ˜„์‹ ํ˜•์‹ํ™”์˜ ํ’ˆ์งˆ์„ ๊ฒ€์ฆํ•˜๋Š” + * ๋ณตํ•ฉ์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ๊ฐ€๋…์„ฑ, ์ •ํ™•์„ฑ, ์ผ๊ด€์„ฑ, + * ์ ‘๊ทผ์„ฑ ๋“ฑ์„ ํ†ตํ•ด ํ˜•์‹ํ™” ๊ฒฐ๊ณผ์˜ ํ’ˆ์งˆ์„ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Specification( + name = "FormattingQuality", + description = "ํ‘œํ˜„์‹ ํ˜•์‹ํ™”์˜ ํ’ˆ์งˆ๊ณผ ์ ์ ˆ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ", + domain = "expresser", + priority = hs.kr.entrydsm.global.annotation.specification.type.Priority.HIGH +) +class FormattingQualitySpec { + + companion object { + private const val MIN_READABILITY_SCORE = 70 + private const val MAX_LINE_LENGTH = 120 + private const val MIN_CONTRAST_RATIO = 4.5 + private const val MAX_NESTING_DEPTH = 10 + private const val MIN_FONT_SIZE = 10 + private const val MAX_COMPLEXITY_SCORE = 100 + + // ๊ฐ€๋…์„ฑ ๊ฐœ์„  ํŒจํ„ด๋“ค + private val READABILITY_PATTERNS = mapOf( + "proper_spacing" to Regex("\\s+[+\\-*/]\\s+"), + "parentheses_balance" to Regex("\\([^()]*\\)"), + "function_formatting" to Regex("[a-zA-Z]\\w*\\s*\\([^)]*\\)"), + "subscript_superscript" to Regex("[a-zA-Z]\\w*[_^]\\{[^}]*\\}") + ) + + // ํ’ˆ์งˆ ๊ธฐ์ค€๋“ค + private val QUALITY_CRITERIA = mapOf( + "readability" to 0.3, + "accuracy" to 0.3, + "consistency" to 0.2, + "accessibility" to 0.1, + "aesthetics" to 0.1 + ) + + // ํ˜•์‹๋ณ„ ํ’ˆ์งˆ ๊ธฐ์ค€ + private val FORMAT_QUALITY_STANDARDS = mapOf( + "latex" to mapOf("math_symbols" to true, "subscript_superscript" to true), + "mathml" to mapOf("semantic_markup" to true, "accessibility" to true), + "html" to mapOf("semantic_tags" to true, "css_styling" to true), + "text" to mapOf("unicode_symbols" to true, "ascii_fallback" to true) + ) + } + + /** + * ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹์ด ํ’ˆ์งˆ ๊ธฐ์ค€์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param formatted ๊ฒ€์ฆํ•  ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + * @param options ์‚ฌ์šฉ๋œ ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ํ’ˆ์งˆ ๊ธฐ์ค€์„ ๋งŒ์กฑํ•˜๋ฉด true + */ + fun isSatisfiedBy(formatted: FormattedExpression, options: FormattingOptions): Boolean { + return try { + val qualityScore = calculateQualityScore(formatted, options) + qualityScore >= MIN_READABILITY_SCORE + } catch (e: Exception) { + false + } + } + + /** + * AST์™€ ํ˜•์‹ํ™” ์˜ต์…˜์œผ๋กœ๋ถ€ํ„ฐ ์˜ˆ์ƒ ํ’ˆ์งˆ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ์›๋ณธ AST + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ์˜ˆ์ƒ ํ’ˆ์งˆ์ด ๊ธฐ์ค€์„ ๋งŒ์กฑํ•˜๋ฉด true + */ + fun isSatisfiedBy(ast: ASTNode, options: FormattingOptions): Boolean { + return try { + validateStructuralComplexity(ast) && + validateFormattingOptions(options) && + predictFormattingQuality(ast, options) >= MIN_READABILITY_SCORE + } catch (e: Exception) { + false + } + } + + /** + * ๊ฐ€๋…์„ฑ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param formatted ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + * @return ๊ฐ€๋…์„ฑ ์ ์ˆ˜ (0-100) + */ + fun calculateReadabilityScore(formatted: FormattedExpression): Int { + var score = 0 + + // ์ ์ ˆํ•œ ๊ณต๋ฐฑ ์‚ฌ์šฉ + score += if (hasProperSpacing(formatted.expression)) 20 else 0 + + // ์ค„ ๊ธธ์ด ์ ์ ˆ์„ฑ + score += if (hasAppropriateLineLength(formatted.expression)) 15 else 0 + + // ๊ด„ํ˜ธ ๊ท ํ˜• + score += if (hasBalancedParentheses(formatted.expression)) 15 else 0 + + // ์ผ๊ด€๋œ ํ˜•์‹ํ™” + score += if (hasConsistentFormatting(formatted.expression)) 20 else 0 + + // ๋ณต์žก๋„ ๊ด€๋ฆฌ + score += if (hasManagedComplexity(formatted.expression)) 15 else 0 + + // ํŠน์ˆ˜ ๋ฌธ์ž ์‚ฌ์šฉ ์ ์ ˆ์„ฑ + score += if (hasAppropriateSymbolUsage(formatted.expression)) 15 else 0 + + return score.coerceIn(0, 100) + } + + /** + * ์ •ํ™•์„ฑ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param original ์›๋ณธ AST + * @param formatted ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + * @return ์ •ํ™•์„ฑ ์ ์ˆ˜ (0-100) + */ + fun calculateAccuracyScore(original: ASTNode, formatted: FormattedExpression): Int { + var score = 0 + + // ์˜๋ฏธ ๋ณด์กด + score += if (preservesMeaning(original, formatted)) 40 else 0 + + // ๊ตฌ์กฐ ๋ณด์กด + score += if (preservesStructure(original, formatted)) 30 else 0 + + // ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๋ณด์กด + score += if (preservesOperatorPrecedence(original, formatted)) 20 else 0 + + // ๋ณ€์ˆ˜๋ช… ๋ณด์กด + score += if (preservesVariableNames(original, formatted)) 10 else 0 + + return score.coerceIn(0, 100) + } + + /** + * ์ผ๊ด€์„ฑ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param formatted ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ์ผ๊ด€์„ฑ ์ ์ˆ˜ (0-100) + */ + fun calculateConsistencyScore(formatted: FormattedExpression, options: FormattingOptions): Int { + var score = 0 + + // ์Šคํƒ€์ผ ์ผ๊ด€์„ฑ + score += if (hasConsistentStyle(formatted, options)) 30 else 0 + + // ๋“ค์—ฌ์“ฐ๊ธฐ ์ผ๊ด€์„ฑ + score += if (hasConsistentIndentation(formatted.expression)) 25 else 0 + + // ์ƒ‰์ƒ ์‚ฌ์šฉ ์ผ๊ด€์„ฑ + score += if (hasConsistentColors(formatted)) 20 else 0 + + // ํฐํŠธ ์‚ฌ์šฉ ์ผ๊ด€์„ฑ + score += if (hasConsistentFonts(formatted)) 15 else 0 + + // ๊ธฐํ˜ธ ์‚ฌ์šฉ ์ผ๊ด€์„ฑ + score += if (hasConsistentSymbols(formatted.expression)) 10 else 0 + + return score.coerceIn(0, 100) + } + + /** + * ์ ‘๊ทผ์„ฑ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param formatted ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + * @return ์ ‘๊ทผ์„ฑ ์ ์ˆ˜ (0-100) + */ + fun calculateAccessibilityScore(formatted: FormattedExpression): Int { + var score = 0 + + // ์ƒ‰์ƒ ๋Œ€๋น„ + score += if (hasAdequateColorContrast(formatted)) 30 else 0 + + // ํฐํŠธ ํฌ๊ธฐ ์ ์ ˆ์„ฑ + score += if (hasAppropriateFont(formatted)) 25 else 0 + + // ์Šคํฌ๋ฆฐ ๋ฆฌ๋” ์ง€์› + score += if (hasScreenReaderSupport(formatted)) 20 else 0 + + // ๋Œ€์ฒด ํ…์ŠคํŠธ + score += if (hasAlternativeText(formatted)) 15 else 0 + + // ํ‚ค๋ณด๋“œ ์ ‘๊ทผ์„ฑ + score += if (hasKeyboardAccessibility(formatted)) 10 else 0 + + return score.coerceIn(0, 100) + } + + /** + * ๋ฏธ์  ํ’ˆ์งˆ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param formatted ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + * @return ๋ฏธ์  ํ’ˆ์งˆ ์ ์ˆ˜ (0-100) + */ + fun calculateAestheticScore(formatted: FormattedExpression): Int { + var score = 0 + + // ์‹œ๊ฐ์  ๊ท ํ˜• + score += if (hasVisualBalance(formatted.expression)) 25 else 0 + + // ์ ์ ˆํ•œ ์—ฌ๋ฐฑ + score += if (hasAppropriateWhitespace(formatted.expression)) 20 else 0 + + // ์ƒ‰์ƒ ์กฐํ™” + score += if (hasHarmoniousColors(formatted)) 20 else 0 + + // ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ํ’ˆ์งˆ + score += if (hasGoodTypography(formatted)) 20 else 0 + + // ์ „์ฒด์ ์ธ ์ •๋ˆ์„ฑ + score += if (hasOverallNeatness(formatted.expression)) 15 else 0 + + return score.coerceIn(0, 100) + } + + /** + * ์ „์ฒด ํ’ˆ์งˆ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param formatted ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @param original ์›๋ณธ AST (์„ ํƒ์ ) + * @return ์ „์ฒด ํ’ˆ์งˆ ์ ์ˆ˜ (0-100) + */ + fun calculateQualityScore( + formatted: FormattedExpression, + options: FormattingOptions, + original: ASTNode? = null + ): Int { + val readabilityScore = calculateReadabilityScore(formatted) + val consistencyScore = calculateConsistencyScore(formatted, options) + val accessibilityScore = calculateAccessibilityScore(formatted) + val aestheticScore = calculateAestheticScore(formatted) + + val accuracyScore = if (original != null) { + calculateAccuracyScore(original, formatted) + } else { + 80 // ๊ธฐ๋ณธ ์ ์ˆ˜ + } + + return (readabilityScore * QUALITY_CRITERIA["readability"]!! + + accuracyScore * QUALITY_CRITERIA["accuracy"]!! + + consistencyScore * QUALITY_CRITERIA["consistency"]!! + + accessibilityScore * QUALITY_CRITERIA["accessibility"]!! + + aestheticScore * QUALITY_CRITERIA["aesthetics"]!!).toInt() + } + + /** + * ๋ณต์žก๋„ ๊ด€๋ฆฌ ํ’ˆ์งˆ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast AST ๋…ธ๋“œ + * @return ๋ณต์žก๋„๊ฐ€ ์ ์ ˆํžˆ ๊ด€๋ฆฌ๋˜๋ฉด true + */ + fun validateComplexityManagement(ast: ASTNode): Boolean { + val complexity = calculateComplexityScore(ast) + return complexity <= MAX_COMPLEXITY_SCORE + } + + /** + * ํ˜•์‹ํ™” ์˜ต์…˜์˜ ์ ์ ˆ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ์ ์ ˆํ•˜๋ฉด true + */ + fun validateFormattingOptions(options: FormattingOptions): Boolean { + return isValidFontSize(options) && + isValidColorScheme(options) && + isValidLayoutSettings(options) + } + + /** + * ํ’ˆ์งˆ ๋ฌธ์ œ๋“ค์„ ์‹๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param formatted ํ˜•์‹ํ™”๋œ ํ‘œํ˜„์‹ + * @param options ํ˜•์‹ํ™” ์˜ต์…˜ + * @return ๋ฐœ๊ฒฌ๋œ ํ’ˆ์งˆ ๋ฌธ์ œ๋“ค + */ + fun identifyQualityIssues(formatted: FormattedExpression, options: FormattingOptions): List { + val issues = mutableListOf() + + // ๊ฐ€๋…์„ฑ ๋ฌธ์ œ + if (!hasProperSpacing(formatted.expression)) { + issues.add(QualityIssue("SPACING", "๋ถ€์ ์ ˆํ•œ ๊ณต๋ฐฑ ์‚ฌ์šฉ", QualityIssue.Severity.MEDIUM)) + } + + if (!hasAppropriateLineLength(formatted.expression)) { + issues.add(QualityIssue("LINE_LENGTH", "์ค„ ๊ธธ์ด ์ดˆ๊ณผ", QualityIssue.Severity.LOW)) + } + + // ์ ‘๊ทผ์„ฑ ๋ฌธ์ œ + if (!hasAdequateColorContrast(formatted)) { + issues.add(QualityIssue("COLOR_CONTRAST", "์ƒ‰์ƒ ๋Œ€๋น„ ๋ถ€์กฑ", QualityIssue.Severity.HIGH)) + } + + if (!hasAppropriateFont(formatted)) { + issues.add(QualityIssue("FONT_SIZE", "๋ถ€์ ์ ˆํ•œ ํฐํŠธ ํฌ๊ธฐ", QualityIssue.Severity.MEDIUM)) + } + + // ์ผ๊ด€์„ฑ ๋ฌธ์ œ + if (!hasConsistentStyle(formatted, options)) { + issues.add(QualityIssue("STYLE_CONSISTENCY", "์Šคํƒ€์ผ ๋ถˆ์ผ์น˜", QualityIssue.Severity.MEDIUM)) + } + + return issues + } + + // Private helper methods + + private fun validateStructuralComplexity(ast: ASTNode): Boolean { + val depth = calculateDepth(ast) + val nodeCount = countNodes(ast) + return depth <= MAX_NESTING_DEPTH && nodeCount <= 1000 + } + + private fun predictFormattingQuality(ast: ASTNode, options: FormattingOptions): Int { + // ์˜ˆ์ƒ ํ’ˆ์งˆ ๊ณ„์‚ฐ (๊ฐ„๋‹จํ•œ ํœด๋ฆฌ์Šคํ‹ฑ) + var predictedScore = 70 // ๊ธฐ๋ณธ ์ ์ˆ˜ + + if (calculateDepth(ast) <= 5) predictedScore += 10 + if (countNodes(ast) <= 50) predictedScore += 10 + if (isValidFontSize(options)) predictedScore += 5 + if (isValidColorScheme(options)) predictedScore += 5 + + return predictedScore.coerceIn(0, 100) + } + + private fun hasProperSpacing(content: String): Boolean { + return READABILITY_PATTERNS["proper_spacing"]?.containsMatchIn(content) ?: false + } + + private fun hasAppropriateLineLength(content: String): Boolean { + return content.lines().all { it.length <= MAX_LINE_LENGTH } + } + + private fun hasBalancedParentheses(content: String): Boolean { + var balance = 0 + for (char in content) { + when (char) { + '(' -> balance++ + ')' -> { + balance-- + if (balance < 0) return false + } + } + } + return balance == 0 + } + + private fun hasConsistentFormatting(content: String): Boolean { + // ์ผ๊ด€๋œ ๋“ค์—ฌ์“ฐ๊ธฐ์™€ ๊ณต๋ฐฑ ์‚ฌ์šฉ ๊ฒ€์‚ฌ + val lines = content.lines() + if (lines.size <= 1) return true + + val indentationPattern = lines.firstOrNull { it.isNotBlank() }?.takeWhile { it.isWhitespace() }?.length ?: 0 + return lines.filter { it.isNotBlank() }.all { line -> + line.takeWhile { it.isWhitespace() }.length % (indentationPattern.coerceAtLeast(1)) == 0 + } + } + + private fun hasManagedComplexity(content: String): Boolean { + val complexity = content.length + content.count { it in "(){}[]" } * 2 + return complexity <= MAX_COMPLEXITY_SCORE + } + + private fun hasAppropriateSymbolUsage(content: String): Boolean { + // ์œ ๋‹ˆ์ฝ”๋“œ ์ˆ˜ํ•™ ๊ธฐํ˜ธ์˜ ์ ์ ˆํ•œ ์‚ฌ์šฉ ๊ฒ€์‚ฌ + val mathSymbols = setOf('โˆ‘', 'โˆ', 'โˆซ', 'โˆš', 'โˆž', 'ฯ€', 'ฮฑ', 'ฮฒ', 'ฮณ', 'ฮธ', 'ฮป', 'ฮผ', 'ฯƒ', 'ฮ”') + val symbolCount = content.count { it in mathSymbols } + return symbolCount <= content.length * 0.1 // ์ „์ฒด ๊ธธ์ด์˜ 10% ์ดํ•˜ + } + + private fun preservesMeaning(original: ASTNode, formatted: FormattedExpression): Boolean { + // ์˜๋ฏธ ๋ณด์กด ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return true // ์‹ค์ œ๋กœ๋Š” ๋” ์ •๊ตํ•œ ์˜๋ฏธ ๋ถ„์„ ํ•„์š” + } + + private fun preservesStructure(original: ASTNode, formatted: FormattedExpression): Boolean { + // ๊ตฌ์กฐ ๋ณด์กด ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return true // ์‹ค์ œ๋กœ๋Š” AST ๊ตฌ์กฐ ๋น„๊ต ํ•„์š” + } + + private fun preservesOperatorPrecedence(original: ASTNode, formatted: FormattedExpression): Boolean { + // ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๋ณด์กด ๊ฒ€์‚ฌ + return !formatted.expression.contains(Regex("\\d\\s*[+\\-]\\s*\\d\\s*[*/]\\s*\\d")) + } + + private fun preservesVariableNames(original: ASTNode, formatted: FormattedExpression): Boolean { + // ๋ณ€์ˆ˜๋ช… ๋ณด์กด ๊ฒ€์‚ฌ + val originalVars = extractVariables(original.toString()) + val formattedVars = extractVariables(formatted.expression) + return originalVars == formattedVars + } + + private fun hasConsistentStyle(formatted: FormattedExpression, options: FormattingOptions): Boolean { + // ์Šคํƒ€์ผ ์ผ๊ด€์„ฑ ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return true + } + + private fun hasConsistentIndentation(content: String): Boolean { + return hasConsistentFormatting(content) + } + + private fun hasConsistentColors(formatted: FormattedExpression): Boolean { + // ์ƒ‰์ƒ ์ผ๊ด€์„ฑ ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return formatted.style.name != "HTML" || formatted.expression.count { it == '#' } <= 5 + } + + private fun hasConsistentFonts(formatted: FormattedExpression): Boolean { + // ํฐํŠธ ์ผ๊ด€์„ฑ ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return formatted.style.name != "HTML" || !formatted.expression.contains("font-family") + } + + private fun hasConsistentSymbols(content: String): Boolean { + // ๊ธฐํ˜ธ ์‚ฌ์šฉ ์ผ๊ด€์„ฑ ๊ฒ€์‚ฌ + return true // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ + } + + private fun hasAdequateColorContrast(formatted: FormattedExpression): Boolean { + // ์ƒ‰์ƒ ๋Œ€๋น„ ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return formatted.style.name != "HTML" || !formatted.expression.contains("color: gray") + } + + private fun hasAppropriateFont(formatted: FormattedExpression): Boolean { + // ํฐํŠธ ํฌ๊ธฐ ์ ์ ˆ์„ฑ ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return !formatted.expression.contains("font-size: [1-9]px") // 10px ๋ฏธ๋งŒ ์ œ์™ธ + } + + private fun hasScreenReaderSupport(formatted: FormattedExpression): Boolean { + return formatted.expression.contains("aria-label") || + formatted.expression.contains("alt=") + } + + private fun hasAlternativeText(formatted: FormattedExpression): Boolean { + return formatted.expression.contains("alt=") || formatted.expression.contains("title=") + } + + private fun hasKeyboardAccessibility(formatted: FormattedExpression): Boolean { + return !formatted.expression.contains("onmouse") && + !formatted.expression.contains("onclick") + } + + private fun hasVisualBalance(content: String): Boolean { + // ์‹œ๊ฐ์  ๊ท ํ˜• ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return content.length <= 200 || content.contains("\n") + } + + private fun hasAppropriateWhitespace(content: String): Boolean { + return !content.contains(Regex("\\S{50,}")) // 50์ž ์ด์ƒ ์—ฐ์† ๋ฌธ์ž ์—†์Œ + } + + private fun hasHarmoniousColors(formatted: FormattedExpression): Boolean { + // ์ƒ‰์ƒ ์กฐํ™” ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return true + } + + private fun hasGoodTypography(formatted: FormattedExpression): Boolean { + // ํƒ€์ดํฌ๊ทธ๋ž˜ํ”ผ ํ’ˆ์งˆ ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return true + } + + private fun hasOverallNeatness(content: String): Boolean { + return !content.contains(Regex("\\s{5,}")) && // 5๊ฐœ ์ด์ƒ ์—ฐ์† ๊ณต๋ฐฑ ์—†์Œ + !content.contains("\t\t\t") // ๊ณผ๋„ํ•œ ํƒญ ์—†์Œ + } + + private fun calculateComplexityScore(ast: ASTNode): Int { + return calculateDepth(ast) * 5 + countNodes(ast) + } + + private fun calculateDepth(ast: ASTNode): Int { + return if (ast.getChildren().isEmpty()) { + 1 + } else { + 1 + (ast.getChildren().maxOfOrNull { calculateDepth(it) } ?: 0) + } + } + + private fun countNodes(ast: ASTNode): Int { + return 1 + ast.getChildren().sumOf { countNodes(it) } + } + + private fun isValidFontSize(options: FormattingOptions): Boolean { + return true // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ + } + + private fun isValidColorScheme(options: FormattingOptions): Boolean { + return true // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ + } + + private fun isValidLayoutSettings(options: FormattingOptions): Boolean { + return true // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ + } + + private fun extractVariables(expression: String): Set { + val pattern = Regex("\\b[a-zA-Z_][a-zA-Z0-9_]*\\b") + return pattern.findAll(expression).map { it.value }.toSet() + } + + /** + * ํ’ˆ์งˆ ๋ฌธ์ œ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class QualityIssue( + val code: String, + val message: String, + val severity: Severity + ) { + enum class Severity { + LOW, MEDIUM, HIGH, CRITICAL + } + } + + /** + * ๋ช…์„ธ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "minReadabilityScore" to MIN_READABILITY_SCORE, + "maxLineLength" to MAX_LINE_LENGTH, + "minContrastRatio" to MIN_CONTRAST_RATIO, + "maxNestingDepth" to MAX_NESTING_DEPTH, + "minFontSize" to MIN_FONT_SIZE, + "maxComplexityScore" to MAX_COMPLEXITY_SCORE, + "qualityCriteria" to QUALITY_CRITERIA, + "readabilityPatterns" to READABILITY_PATTERNS.size + ) + + /** + * ๋ช…์„ธ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "specificationName" to "FormattingQualitySpec", + "qualityDimensions" to QUALITY_CRITERIA.size, + "readabilityPatterns" to READABILITY_PATTERNS.size, + "formatQualityStandards" to FORMAT_QUALITY_STANDARDS.size, + "qualityChecks" to listOf("readability", "accuracy", "consistency", "accessibility", "aesthetics") + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/values/FormattedExpression.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/values/FormattedExpression.kt new file mode 100644 index 00000000..71bf767c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/expresser/values/FormattedExpression.kt @@ -0,0 +1,426 @@ +package hs.kr.entrydsm.domain.expresser.values + +import hs.kr.entrydsm.domain.expresser.entities.FormattingOptions +import hs.kr.entrydsm.domain.expresser.entities.FormattingStyle + +/** + * ํฌ๋งทํŒ…๋œ ์ˆ˜์‹ ํ‘œํ˜„์„ ๋‹ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ํฌ๋งทํŒ… ๊ณผ์ •์„ ๊ฑฐ์นœ ์ˆ˜์‹ ๋ฌธ์ž์—ด๊ณผ ํ•จ๊ป˜ ์ ์šฉ๋œ ์Šคํƒ€์ผ๊ณผ ์˜ต์…˜ ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋˜์–ด ์•ˆ์ „ํ•œ ์ˆ˜์‹ ํ‘œํ˜„์„ ๋ณด์žฅํ•˜๋ฉฐ, ๋‹ค์–‘ํ•œ ๋ถ„์„ ๋ฐ ๋ณ€ํ™˜ ๋ฉ”์„œ๋“œ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @property expression ํฌ๋งทํŒ…๋œ ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @property style ์ ์šฉ๋œ ํฌ๋งทํŒ… ์Šคํƒ€์ผ + * @property options ์ ์šฉ๋œ ํฌ๋งทํŒ… ์˜ต์…˜ + * @property length ์ˆ˜์‹ ๋ฌธ์ž์—ด์˜ ๊ธธ์ด + * @property createdAt ์ƒ์„ฑ ์‹œ๊ฐ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +data class FormattedExpression( + val expression: String, + val style: FormattingStyle, + val options: FormattingOptions, + val length: Int = expression.length, + val createdAt: Long = System.currentTimeMillis() +) { + + init { + if (expression.isBlank()) { + throw hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException.formattingError( + "expression", "ํฌ๋งทํŒ…๋œ ์ˆ˜์‹์€ ๊ณต๋ฐฑ์ด ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + } + if (length < 0) { + throw hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException.invalidFormatOption( + "length=$length", "์ˆ˜์‹ ๊ธธ์ด๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + } + } + + /** + * ์ˆ˜์‹์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆ˜์‹์ด ๋น„์–ด์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isEmpty(): Boolean = expression.isBlank() + + /** + * ์ˆ˜์‹์ด ๋น„์–ด์žˆ์ง€ ์•Š์€์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆ˜์‹์ด ๋น„์–ด์žˆ์ง€ ์•Š์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isNotEmpty(): Boolean = !isEmpty() + + /** + * ์ˆ˜์‹์ด ๋‹จ์ผ ํ† ํฐ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹จ์ผ ํ† ํฐ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSingleToken(): Boolean = !expression.contains(" ") && !expression.contains("(") && !expression.contains(")") + + /** + * ์ˆ˜์‹์ด ๋ณตํ•ฉ ํ‘œํ˜„์‹์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณตํ•ฉ ํ‘œํ˜„์‹์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isComplex(): Boolean = expression.contains("(") || expression.contains("+") || expression.contains("-") || + expression.contains("*") || expression.contains("/") || expression.contains("^") + + /** + * ์ˆ˜์‹์— ํŠน์ • ์—ฐ์‚ฐ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ํ™•์ธํ•  ์—ฐ์‚ฐ์ž + * @return ์—ฐ์‚ฐ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun containsOperator(operator: String): Boolean = expression.contains(operator) + + /** + * ์ˆ˜์‹์— ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun containsFunctionCall(): Boolean = expression.contains("(") && expression.contains(")") + + /** + * ์ˆ˜์‹์— ๋ณ€์ˆ˜๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€์ˆ˜๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun containsVariable(): Boolean = expression.any { it.isLetter() && it != 'e' && it != 'ฯ€' } + + /** + * ์ˆ˜์‹์— ์ˆซ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆซ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun containsNumber(): Boolean = expression.any { it.isDigit() } + + /** + * ์ˆ˜์‹์— ํŠน์ˆ˜ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŠน์ˆ˜ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun containsSpecialCharacters(): Boolean = expression.any { + it in "ร—รทโ‰ โ‰คโ‰ฅโˆงโˆจโˆšฯ€โˆž" || it == '\\' || it == '{' || it == '}' + } + + /** + * ์ˆ˜์‹์˜ ๋ณต์žก๋„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณต์žก๋„ ์ ์ˆ˜ (0-100) + */ + fun calculateComplexity(): Int { + var complexity = 0 + + // ๊ธธ์ด์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += (length / LENGTH_WEIGHT_DIVISOR).coerceAtMost(MAX_LENGTH_COMPLEXITY) + + // ์—ฐ์‚ฐ์ž ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += COMPLEXITY_OPERATORS.sumOf { op -> + expression.count { it.toString() == op } * OPERATOR_WEIGHT + } + + // ๊ด„ํ˜ธ ๊ฐœ์ˆ˜์— ๋”ฐ๋ฅธ ๋ณต์žก๋„ + complexity += expression.count { it == '(' } * PARENTHESES_WEIGHT + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ฐœ์ˆ˜์— ๋”ฐํ•œ ๋ณต์žก๋„ + complexity += expression.count { it.isLetter() && expression.indexOf(it) < expression.indexOf('(') } * FUNCTION_WEIGHT + + return complexity.coerceAtMost(MAX_COMPLEXITY_SCORE) + } + + /** + * ์ˆ˜์‹์˜ ๊ฐ€๋…์„ฑ์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ€๋…์„ฑ ์ ์ˆ˜ (0-100) + */ + fun calculateReadability(): Int { + var readability = 100 + + // ๊ธธ์ด์— ๋”ฐ๋ฅธ ๊ฐ€๋…์„ฑ ๊ฐ์†Œ + if (length > 50) readability -= (length - 50) / 2 + + // ์ค‘์ฒฉ ๊ด„ํ˜ธ์— ๋”ฐ๋ฅธ ๊ฐ€๋…์„ฑ ๊ฐ์†Œ + var maxNesting = 0 + var currentNesting = 0 + expression.forEach { char -> + when (char) { + '(' -> currentNesting++ + ')' -> currentNesting-- + } + maxNesting = maxOf(maxNesting, currentNesting) + } + readability -= maxNesting * 5 + + // ์Šคํƒ€์ผ์— ๋”ฐ๋ฅธ ๊ฐ€๋…์„ฑ ์กฐ์ • + readability += when(style) { + FormattingStyle.MATHEMATICAL -> 4 + FormattingStyle.PROGRAMMING -> 3 + FormattingStyle.LATEX -> 2 + FormattingStyle.VERBOSE -> 5 + FormattingStyle.COMPACT -> 1 + } * 5 + + // ๊ณต๋ฐฑ ์‚ฌ์šฉ์— ๋”ฐ๋ฅธ ๊ฐ€๋…์„ฑ ํ–ฅ์ƒ + if (options.addSpaces) readability += 10 + + return readability.coerceIn(0, 100) + } + + /** + * ์ˆ˜์‹์„ ๋‹ค๋ฅธ ์Šคํƒ€์ผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newStyle ์ƒˆ๋กœ์šด ์Šคํƒ€์ผ + * @return ์ƒˆ๋กœ์šด ์Šคํƒ€์ผ๋กœ ๋ณ€ํ™˜๋œ FormattedExpression + */ + fun convertToStyle(newStyle: FormattingStyle): FormattedExpression { + val newOptions = options.withStyle(newStyle).adjustForStyle() + return copy( + style = newStyle, + options = newOptions, + createdAt = System.currentTimeMillis() + ) + } + + /** + * ์ˆ˜์‹์„ ์••์ถ•ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์••์ถ•๋œ FormattedExpression + */ + fun compress(): FormattedExpression { + val compressed = expression + .replace(" ", "") + .replace("ร—", "*") + .replace("รท", "/") + .replace("โ‰ ", "!=") + .replace("โ‰ค", "<=") + .replace("โ‰ฅ", ">=") + .replace("โˆง", "&&") + .replace("โˆจ", "||") + .replace("โˆš", "sqrt") + .replace("ฯ€", "pi") + + return copy( + expression = compressed, + style = FormattingStyle.COMPACT, + options = FormattingOptions.compact(), + length = compressed.length, + createdAt = System.currentTimeMillis() + ) + } + + /** + * ์ˆ˜์‹์„ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ™•์žฅ๋œ FormattedExpression + */ + fun expand(): FormattedExpression { + val expanded = expression + .replace("*", " ร— ") + .replace("/", " รท ") + .replace("+", " + ") + .replace("-", " - ") + .replace("==", " = ") + .replace("!=", " โ‰  ") + .replace("<=", " โ‰ค ") + .replace(">=", " โ‰ฅ ") + .replace("&&", " โˆง ") + .replace("||", " โˆจ ") + .replace("sqrt", "โˆš") + .replace("pi", "ฯ€") + + return copy( + expression = expanded, + style = FormattingStyle.MATHEMATICAL, + options = FormattingOptions.mathematical(), + length = expanded.length, + createdAt = System.currentTimeMillis() + ) + } + + /** + * ์ˆ˜์‹์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "length" to length, + "wordCount" to expression.split(" ").size, + "operatorCount" to expression.count { it in "+-*/^=<>!&|" }, + "parenthesesCount" to expression.count { it == '(' }, + "functionCallCount" to expression.count { it.isLetter() && expression.indexOf(it) < expression.indexOf('(') }, + "variableCount" to expression.count { it.isLetter() && it != 'e' && it != 'ฯ€' }, + "numberCount" to expression.count { it.isDigit() }, + "specialCharCount" to expression.count { it in "ร—รทโ‰ โ‰คโ‰ฅโˆงโˆจโˆšฯ€โˆž" }, + "complexity" to calculateComplexity(), + "readability" to calculateReadability(), + "isSingleToken" to isSingleToken(), + "isComplex" to isComplex(), + "containsFunctionCall" to containsFunctionCall(), + "containsVariable" to containsVariable(), + "containsNumber" to containsNumber(), + "containsSpecialCharacters" to containsSpecialCharacters(), + "style" to style.name, + "createdAt" to createdAt + ) + + /** + * ์ˆ˜์‹์„ JSON ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return JSON ํ˜•ํƒœ์˜ ์ˆ˜์‹ ํ‘œํ˜„ + */ + fun toJson(): String = buildString { + append("{") + append("\"expression\": \"${expression.replace("\"", "\\\"")}\",") + append("\"style\": \"${style.name}\",") + append("\"length\": $length,") + append("\"complexity\": ${calculateComplexity()},") + append("\"readability\": ${calculateReadability()},") + append("\"createdAt\": $createdAt") + append("}") + } + + /** + * ์ˆ˜์‹์„ XML ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return XML ํ˜•ํƒœ์˜ ์ˆ˜์‹ ํ‘œํ˜„ + */ + fun toXml(): String = buildString { + appendLine("") + appendLine(" ") + appendLine(" ") + appendLine(" $length") + appendLine(" ${calculateComplexity()}") + appendLine(" ${calculateReadability()}") + appendLine(" $createdAt") + appendLine("") + } + + /** + * ์ˆ˜์‹์˜ ํ•ด์‹œ๊ฐ’์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•ด์‹œ๊ฐ’ + */ + fun calculateHash(): String = (expression + style.name + options.toString()).hashCode().toString(16) + + /** + * ๋‹ค๋ฅธ FormattedExpression๊ณผ ์˜๋ฏธ์ ์œผ๋กœ ๋™์ผํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  FormattedExpression + * @return ์˜๋ฏธ์ ์œผ๋กœ ๋™์ผํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSemanticallyEqual(other: FormattedExpression): Boolean { + val thisNormalized = this.compress().expression + val otherNormalized = other.compress().expression + return thisNormalized == otherNormalized + } + + /** + * ์ˆ˜์‹์˜ ์˜ˆ์ƒ ์ถœ๋ ฅ ๋„ˆ๋น„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ˆ์ƒ ์ถœ๋ ฅ ๋„ˆ๋น„ (๋ฌธ์ž ์ˆ˜) + */ + fun calculateDisplayWidth(): Int { + var width = 0 + expression.forEach { char -> + width += when (char) { + in "ร—รทโ‰ โ‰คโ‰ฅโˆงโˆจโˆšฯ€โˆž" -> 2 // ํŠน์ˆ˜ ๋ฌธ์ž๋Š” 2๋ฐฐ ํญ + else -> 1 + } + } + return width + } + + /** + * ์ˆ˜์‹์„ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฝ๊ธฐ ์‰ฌ์šด ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + override fun toString(): String = expression + + /** + * ์ˆ˜์‹์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ํฌํ•จํ•œ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋ฌธ์ž์—ด + */ + fun toDetailString(): String = buildString { + append("FormattedExpression(") + append("expression=\"$expression\", ") + append("style=${style.name}, ") + append("length=$length, ") + append("complexity=${calculateComplexity()}, ") + append("readability=${calculateReadability()}") + append(")") + } + + companion object { + /** + * ๋ณต์žก๋„ ๊ณ„์‚ฐ์— ์‚ฌ์šฉ๋˜๋Š” ์—ฐ์‚ฐ์ž ๋ชฉ๋ก + */ + private val COMPLEXITY_OPERATORS = listOf( + "+", "-", "*", "/", "^", "==", "!=", "<", ">", "<=", ">=", "&&", "||" + ) + + /** + * ๋ณต์žก๋„ ๊ณ„์‚ฐ ๊ฐ€์ค‘์น˜ + */ + private const val LENGTH_WEIGHT_DIVISOR = 10 + private const val MAX_LENGTH_COMPLEXITY = 20 + private const val OPERATOR_WEIGHT = 5 + private const val PARENTHESES_WEIGHT = 3 + private const val FUNCTION_WEIGHT = 8 + private const val MAX_COMPLEXITY_SCORE = 100 + + /** + * ๋นˆ ํ‘œํ˜„์‹์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋นˆ FormattedExpression + */ + fun empty(): FormattedExpression = FormattedExpression( + expression = " ", + style = FormattingStyle.MATHEMATICAL, + options = FormattingOptions.default() + ) + + /** + * ๊ฐ„๋‹จํ•œ ํ‘œํ˜„์‹์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ํ‘œํ˜„์‹ ๋ฌธ์ž์—ด + * @return FormattedExpression + */ + fun simple(expression: String): FormattedExpression = FormattedExpression( + expression = expression, + style = FormattingStyle.MATHEMATICAL, + options = FormattingOptions.default() + ) + + /** + * ์—ฌ๋Ÿฌ ํ‘œํ˜„์‹์„ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param expressions ๊ฒฐํ•ฉํ•  ํ‘œํ˜„์‹๋“ค + * @param separator ๊ตฌ๋ถ„์ž + * @return ๊ฒฐํ•ฉ๋œ FormattedExpression + */ + fun combine(expressions: List, separator: String = ", "): FormattedExpression { + if (expressions.isEmpty()) { + throw hs.kr.entrydsm.domain.expresser.exceptions.ExpresserException.formattingError( + "combine", "๊ฒฐํ•ฉํ•  ํ‘œํ˜„์‹์ด ์—†์Šต๋‹ˆ๋‹ค" + ) + } + + val combined = expressions.joinToString(separator) { it.expression } + val firstStyle = expressions.first().style + val firstOptions = expressions.first().options + + return FormattedExpression( + expression = combined, + style = firstStyle, + options = firstOptions + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/factories/EnvironmentFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/factories/EnvironmentFactory.kt new file mode 100644 index 00000000..2990c025 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/factories/EnvironmentFactory.kt @@ -0,0 +1,224 @@ +package hs.kr.entrydsm.domain.factories + +import kotlin.math.* + +/** + * ๊ณ„์‚ฐ ํ™˜๊ฒฝ ์ƒ์„ฑ์„ ๋‹ด๋‹นํ•˜๋Š” ์‹ฑ๊ธ€ํ†ค ํŒฉํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. + * + * DRY ์›์น™์„ ์ ์šฉํ•˜์—ฌ CalculatorFactory์™€ EvaluatorFactory์—์„œ ์ค‘๋ณต๋˜๋˜ + * ํ™˜๊ฒฝ ์ƒ์„ฑ ๋กœ์ง์„ ํ†ตํ•ฉํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ๊ณ„์‚ฐ ํ™˜๊ฒฝ์— ํ•„์š”ํ•œ ์ƒ์ˆ˜์™€ ๋ณ€์ˆ˜๋“ค์„ + * ์ผ๊ด€๋˜๊ฒŒ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.06 + */ +object EnvironmentFactory { + + /** + * ๊ธฐ๋ณธ ์ˆ˜ํ•™ ์ƒ์ˆ˜๋“ค์„ ํฌํ•จํ•œ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๋ชจ๋“  ํ™˜๊ฒฝ์˜ ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ํ•ต์‹ฌ ์ƒ์ˆ˜๋“ค์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์ƒ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ํ™˜๊ฒฝ ๋งต + */ + fun createBasicEnvironment(): Map { + return mapOf( + "PI" to PI, + "E" to E, + "TRUE" to true, + "FALSE" to false, + "INFINITY" to Double.POSITIVE_INFINITY, + "NAN" to Double.NaN + ) + } + + /** + * ์ˆ˜ํ•™ ๊ณ„์‚ฐ์šฉ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๊ธฐ๋ณธ ํ™˜๊ฒฝ์— ์ถ”๊ฐ€์ ์ธ ์ˆ˜ํ•™ ์ƒ์ˆ˜๋“ค์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆ˜ํ•™ ์ƒ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ํ™˜๊ฒฝ ๋งต + */ + fun createMathEnvironment(): Map { + val environment = createBasicEnvironment().toMutableMap() + + // ์ถ”๊ฐ€ ์ˆ˜ํ•™ ์ƒ์ˆ˜๋“ค + environment["GOLDEN_RATIO"] = (1 + sqrt(5.0)) / 2 + environment["EULER_GAMMA"] = 0.5772156649015329 + environment["SQRT_2"] = sqrt(2.0) + environment["SQRT_PI"] = sqrt(PI) + environment["SQRT_2PI"] = sqrt(2 * PI) + environment["LN_2"] = ln(2.0) + environment["LN_10"] = ln(10.0) + environment["LOG10_E"] = log10(E) + + return environment + } + + /** + * ๊ณผํ•™ ๊ณ„์‚ฐ์šฉ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์ˆ˜ํ•™ ํ™˜๊ฒฝ์— ๋ฌผ๋ฆฌ ์ƒ์ˆ˜๋“ค์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณผํ•™ ์ƒ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ํ™˜๊ฒฝ ๋งต + */ + fun createScientificEnvironment(): Map { + val environment = createMathEnvironment().toMutableMap() + + // ๊ธฐ๋ณธ ๋ฌผ๋ฆฌ ์ƒ์ˆ˜๋“ค + environment["LIGHT_SPEED"] = 299792458.0 // m/s + environment["PLANCK"] = 6.62607015e-34 // Jโ‹…s + environment["PLANCK_REDUCED"] = 1.054571817e-34 // โ„ (Jโ‹…s) + environment["AVOGADRO"] = 6.02214076e23 // molโปยน + environment["BOLTZMANN"] = 1.380649e-23 // J/K + environment["GAS_CONSTANT"] = 8.314462618 // J/(molโ‹…K) + + // ์ž…์ž ๋ฌผ๋ฆฌ ์ƒ์ˆ˜๋“ค + environment["ELECTRON_CHARGE"] = 1.602176634e-19 // C + environment["ELECTRON_MASS"] = 9.1093837015e-31 // kg + environment["PROTON_MASS"] = 1.67262192369e-27 // kg + environment["NEUTRON_MASS"] = 1.67492749804e-27 // kg + environment["FINE_STRUCTURE"] = 7.2973525693e-3 // ๋ฌด์ฐจ์› + + return environment + } + + /** + * ๊ณตํ•™ ๊ณ„์‚ฐ์šฉ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๊ณผํ•™ ํ™˜๊ฒฝ์— ๊ณตํ•™ ์ƒ์ˆ˜๋“ค์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณตํ•™ ์ƒ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ํ™˜๊ฒฝ ๋งต + */ + fun createEngineeringEnvironment(): Map { + val environment = createScientificEnvironment().toMutableMap() + + // ๊ณตํ•™ ์ƒ์ˆ˜๋“ค + environment["GRAVITY"] = 9.80665 // m/sยฒ (ํ‘œ์ค€ ์ค‘๋ ฅ๊ฐ€์†๋„) + environment["ATMOSPHERIC_PRESSURE"] = 101325.0 // Pa (ํ‘œ์ค€ ๋Œ€๊ธฐ์••) + environment["ABSOLUTE_ZERO"] = -273.15 // ยฐC (์ ˆ๋Œ€ ์˜๋„) + environment["WATER_DENSITY"] = 1000.0 // kg/mยณ (๋ฌผ์˜ ๋ฐ€๋„, 4ยฐC) + environment["AIR_DENSITY"] = 1.225 // kg/mยณ (๊ณต๊ธฐ ๋ฐ€๋„, 15ยฐC, 1 atm) + environment["SOUND_SPEED_AIR"] = 343.0 // m/s (๊ณต๊ธฐ ์ค‘ ์Œ์†, 20ยฐC) + environment["STEFAN_BOLTZMANN"] = 5.670374419e-8 // W/(mยฒโ‹…Kโด) + + return environment + } + + /** + * ํ†ต๊ณ„ ๊ณ„์‚ฐ์šฉ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๊ธฐ๋ณธ ํ™˜๊ฒฝ์— ํ†ต๊ณ„ ๊ด€๋ จ ์ƒ์ˆ˜๋“ค์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ƒ์ˆ˜๊ฐ€ ํฌํ•จ๋œ ํ™˜๊ฒฝ ๋งต + */ + fun createStatisticalEnvironment(): Map { + val environment = createBasicEnvironment().toMutableMap() + + // ํ†ต๊ณ„ ์ƒ์ˆ˜๋“ค + environment["SQRT_2"] = sqrt(2.0) + environment["SQRT_PI"] = sqrt(PI) + environment["SQRT_2PI"] = sqrt(2 * PI) + environment["LN_2"] = ln(2.0) + environment["LN_10"] = ln(10.0) + environment["LOG10_E"] = log10(E) + environment["GOLDEN_RATIO"] = (1 + sqrt(5.0)) / 2 + environment["EULER_GAMMA"] = 0.5772156649015329 + + // ํ™•๋ฅ  ๋ถ„ํฌ ๊ด€๋ จ ์ƒ์ˆ˜๋“ค + environment["NORMAL_CONSTANT"] = 1.0 / sqrt(2 * PI) + environment["CHI_SQUARE_CONSTANT"] = ln(2.0) / 2 + + return environment + } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๊ธฐ๋ณธ ํ™˜๊ฒฝ์— ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ๋ณ€์ˆ˜๋“ค์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param customVariables ์‚ฌ์šฉ์ž ์ •์˜ ๋ณ€์ˆ˜๋“ค + * @param baseEnvironment ๊ธฐ๋ฐ˜์ด ๋  ํ™˜๊ฒฝ (๊ธฐ๋ณธ๊ฐ’: ๊ธฐ๋ณธ ํ™˜๊ฒฝ) + * @return ์‚ฌ์šฉ์ž ์ •์˜ ๋ณ€์ˆ˜๊ฐ€ ํฌํ•จ๋œ ํ™˜๊ฒฝ ๋งต + */ + fun createCustomEnvironment( + customVariables: Map, + baseEnvironment: Map = createBasicEnvironment() + ): Map { + return baseEnvironment.toMutableMap().apply { + putAll(customVariables) + } + } + + /** + * ์ง€์ •๋œ ํ™˜๊ฒฝ ํƒ€์ž…์— ๋”ฐ๋ผ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param environmentType ํ™˜๊ฒฝ ํƒ€์ž… + * @param customVariables ์ถ”๊ฐ€ํ•  ์‚ฌ์šฉ์ž ์ •์˜ ๋ณ€์ˆ˜๋“ค (์„ ํƒ์ ) + * @return ์ง€์ •๋œ ํƒ€์ž…์˜ ํ™˜๊ฒฝ ๋งต + */ + fun createEnvironment( + environmentType: EnvironmentType, + customVariables: Map = emptyMap() + ): Map { + val baseEnvironment = when (environmentType) { + EnvironmentType.BASIC -> createBasicEnvironment() + EnvironmentType.MATH -> createMathEnvironment() + EnvironmentType.SCIENTIFIC -> createScientificEnvironment() + EnvironmentType.ENGINEERING -> createEngineeringEnvironment() + EnvironmentType.STATISTICAL -> createStatisticalEnvironment() + } + + return if (customVariables.isNotEmpty()) { + createCustomEnvironment(customVariables, baseEnvironment) + } else { + baseEnvironment + } + } + + /** + * ํ™˜๊ฒฝ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class EnvironmentType { + BASIC, + MATH, + SCIENTIFIC, + ENGINEERING, + STATISTICAL + } + + /** + * ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์ƒ์ˆ˜๋“ค์˜ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์ˆ˜๋ช…๊ณผ ์„ค๋ช…์ด ํฌํ•จ๋œ ๋งต + */ + fun getAvailableConstants(): Map = mapOf( + "PI" to "์›์ฃผ์œจ (3.14159...)", + "E" to "์ž์—ฐ์ƒ์ˆ˜ (2.71828...)", + "GOLDEN_RATIO" to "ํ™ฉ๊ธˆ๋น„ (1.61803...)", + "EULER_GAMMA" to "์˜ค์ผ๋Ÿฌ-๋งˆ์Šค์ผ€๋กœ๋‹ˆ ์ƒ์ˆ˜ (0.57721...)", + "LIGHT_SPEED" to "๊ด‘์† (299,792,458 m/s)", + "PLANCK" to "ํ”Œ๋ž‘ํฌ ์ƒ์ˆ˜ (6.626... ร— 10โปยณโด Jโ‹…s)", + "AVOGADRO" to "์•„๋ณด๊ฐ€๋“œ๋กœ ์ˆ˜ (6.022... ร— 10ยฒยณ molโปยน)", + "BOLTZMANN" to "๋ณผ์ธ ๋งŒ ์ƒ์ˆ˜ (1.380... ร— 10โปยฒยณ J/K)", + "GAS_CONSTANT" to "๊ธฐ์ฒด์ƒ์ˆ˜ (8.314 J/(molโ‹…K))", + "GRAVITY" to "ํ‘œ์ค€ ์ค‘๋ ฅ๊ฐ€์†๋„ (9.80665 m/sยฒ)", + "ATMOSPHERIC_PRESSURE" to "ํ‘œ์ค€ ๋Œ€๊ธฐ์•• (101,325 Pa)" + ) + + /** + * ํ™˜๊ฒฝ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param environmentType ํ™˜๊ฒฝ ํƒ€์ž… + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getEnvironmentInfo(environmentType: EnvironmentType): Map { + val environment = createEnvironment(environmentType) + return mapOf( + "type" to environmentType.name, + "constantCount" to environment.size, + "constants" to environment.keys.sorted(), + "description" to when (environmentType) { + EnvironmentType.BASIC -> "๊ธฐ๋ณธ ์ˆ˜ํ•™ ์ƒ์ˆ˜๋“ค" + EnvironmentType.MATH -> "์ˆ˜ํ•™ ๊ณ„์‚ฐ์šฉ ์ƒ์ˆ˜๋“ค" + EnvironmentType.SCIENTIFIC -> "๊ณผํ•™ ๊ณ„์‚ฐ์šฉ ์ƒ์ˆ˜๋“ค" + EnvironmentType.ENGINEERING -> "๊ณตํ•™ ๊ณ„์‚ฐ์šฉ ์ƒ์ˆ˜๋“ค" + EnvironmentType.STATISTICAL -> "ํ†ต๊ณ„ ๊ณ„์‚ฐ์šฉ ์ƒ์ˆ˜๋“ค" + } + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/aggregates/LexerAggregate.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/aggregates/LexerAggregate.kt new file mode 100644 index 00000000..0778cf78 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/aggregates/LexerAggregate.kt @@ -0,0 +1,460 @@ +package hs.kr.entrydsm.domain.lexer.aggregates + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenPosition +import hs.kr.entrydsm.domain.lexer.exceptions.LexerException +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.domain.lexer.factories.TokenFactory +import hs.kr.entrydsm.domain.lexer.contract.LexerContract +import hs.kr.entrydsm.domain.lexer.policies.CharacterRecognitionPolicy +import hs.kr.entrydsm.domain.lexer.policies.TokenValidationPolicy +import hs.kr.entrydsm.domain.lexer.specifications.InputValiditySpec +import hs.kr.entrydsm.domain.lexer.specifications.TokenValidationSpec +import hs.kr.entrydsm.domain.lexer.values.LexingContext +import hs.kr.entrydsm.domain.lexer.values.LexingResult +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import hs.kr.entrydsm.global.values.Position + +/** + * Lexer ๋„๋ฉ”์ธ์˜ ํ•ต์‹ฌ Aggregate Root์ž…๋‹ˆ๋‹ค. + * + * DDD Aggregate ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์–ดํœ˜ ๋ถ„์„์˜ ๋ชจ๋“  ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง๊ณผ + * ๊ทœ์น™์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ž…๋ ฅ ํ…์ŠคํŠธ๋ฅผ ํ† ํฐ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” + * ํ•ต์‹ฌ ์ฑ…์ž„์„ ๊ฐ€์ง€๋ฉฐ, ์ผ๊ด€์„ฑ ๊ฒฝ๊ณ„๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Aggregate(context = "lexer") +class LexerAggregate : LexerContract { + + private val tokenFactory = TokenFactory() + private val tokenValidationPolicy = TokenValidationPolicy() + private val characterRecognitionPolicy = CharacterRecognitionPolicy() + private val inputValiditySpec = InputValiditySpec() + private val tokenValidationSpec = TokenValidationSpec() + + private var debugMode: Boolean = false + private var errorRecoveryMode: Boolean = true + private var statistics = mutableMapOf() + + init { + resetStatistics() + } + + /** + * ์ฃผ์–ด์ง„ ์ž…๋ ฅ ํ…์ŠคํŠธ๋ฅผ ์–ดํœ˜ ๋ถ„์„ํ•˜์—ฌ ํ† ํฐ ๋ชฉ๋ก์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + override fun tokenize(input: String): LexingResult { + val context = LexingContext.of(input) + return tokenize(context) + } + + /** + * ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์–ดํœ˜ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun tokenize(context: LexingContext): LexingResult { + val startTime = System.currentTimeMillis() + + try { + // ์ž…๋ ฅ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + if (!inputValiditySpec.isValidContext(context)) { + throw LexerException(ErrorCode.VALIDATION_FAILED) + } + + val tokens = mutableListOf() + var currentContext = context + + // ํ† ํฐํ™” ์ˆ˜ํ–‰ + while (currentContext.hasNext()) { + val (token, newContext) = nextToken(currentContext) + + if (token != null) { + // ํ† ํฐ ๊ฒ€์ฆ + if (tokenValidationSpec.isSatisfiedBy(token)) { + tokens.add(token) + updateStatistics("tokensGenerated", (statistics["tokensGenerated"] as Int) + 1) + } + } + + currentContext = newContext + } + + // EOF ํ† ํฐ ์ถ”๊ฐ€ + val eofToken = createEOFToken(currentContext) + tokens.add(eofToken) + + val duration = System.currentTimeMillis() - startTime + updateStatistics("totalProcessingTime", (statistics["totalProcessingTime"] as Long) + duration) + updateStatistics("inputsProcessed", (statistics["inputsProcessed"] as Int) + 1) + + return LexingResult.success(tokens, duration, context.input.length) + + } catch (e: Exception) { + val duration = System.currentTimeMillis() - startTime + val lexerException = if (e is LexerException) e else + LexerException(ErrorCode.UNKNOWN_ERROR, message = "์–ดํœ˜ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", cause = e) + + updateStatistics("errorsOccurred", (statistics["errorsOccurred"] as Int) + 1) + + return LexingResult.failure(lexerException, emptyList(), duration, context.input.length) + } + } + + /** + * ์ž…๋ ฅ ํ…์ŠคํŠธ์—์„œ ๋‹ค์Œ ํ† ํฐ ํ•˜๋‚˜๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + */ + override fun nextToken(context: LexingContext): Pair { + var currentContext = skipWhitespace(context) + + if (!currentContext.hasNext()) { + return null to currentContext + } + + val currentChar = currentContext.currentChar ?: return null to currentContext + + return when { + characterRecognitionPolicy.isDigit(currentChar) -> parseNumber(currentContext) + characterRecognitionPolicy.isIdentifierStart(currentChar) -> parseIdentifier(currentContext) + characterRecognitionPolicy.isVariableDelimiter(currentChar) && currentChar == '{' -> parseVariable(currentContext) + characterRecognitionPolicy.isOperatorStart(currentChar) -> parseOperator(currentContext) + characterRecognitionPolicy.isDelimiter(currentChar) -> parseDelimiter(currentContext) + else -> throw LexerException.unexpectedCharacter(currentChar, currentContext.currentPosition.index) + } + } + + /** + * ํ˜„์žฌ ์œ„์น˜์—์„œ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ(lookahead) ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun peekTokens(context: LexingContext, count: Int): List { + val tokens = mutableListOf() + var currentContext = context + + repeat(count) { + if (currentContext.hasNext()) { + val (token, newContext) = nextToken(currentContext) + if (token != null) { + tokens.add(token) + currentContext = newContext + } + } + } + + return tokens + } + + /** + * ์ฃผ์–ด์ง„ ์ž…๋ ฅ์ด ์œ ํšจํ•œ ํ† ํฐ๋“ค๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + override fun validate(input: String): Boolean { + return try { + inputValiditySpec.isSatisfiedBy(input) + val result = tokenize(input) + result.isSuccess && tokenValidationSpec.areAllTokensValid(result.tokens) + } catch (e: Exception) { + false + } + } + + /** + * ์ฃผ์–ด์ง„ ์ปจํ…์ŠคํŠธ๊ฐ€ ์œ ํšจํ•œ ์ƒํƒœ์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + override fun validateContext(context: LexingContext): Boolean { + return inputValiditySpec.isValidContext(context) + } + + /** + * ํ˜„์žฌ ์œ„์น˜๊ฐ€ ํŠน์ • ํ† ํฐ ํƒ€์ž…์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + override fun isTokenTypeAt(context: LexingContext, vararg expectedTypes: TokenType): Boolean { + if (!context.hasNext()) return false + + val (token, _) = nextToken(context) + return token?.type in expectedTypes + } + + /** + * ๊ณต๋ฐฑ ๋ฌธ์ž๋“ค์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋‹ค์Œ ์œ ํšจํ•œ ์œ„์น˜๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun skipWhitespace(context: LexingContext): LexingContext { + if (!context.skipWhitespace) return context + + var currentContext = context + + while (currentContext.hasNext()) { + val currentChar = currentContext.currentChar + if (currentChar != null && characterRecognitionPolicy.isWhitespace(currentChar)) { + currentContext = currentContext.advance() + } else { + break + } + } + + return currentContext + } + + /** + * ์ฃผ์„์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋‹ค์Œ ์œ ํšจํ•œ ์œ„์น˜๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun skipComments(context: LexingContext): LexingContext { + var currentContext = context + + while (currentContext.hasNext()) { + val currentChar = currentContext.currentChar ?: break + + if (characterRecognitionPolicy.isCommentStart(currentChar)) { + currentContext = when (currentChar) { + '/' -> { + val nextChar = currentContext.peekChar() + when (nextChar) { + '/' -> skipLineComment(currentContext) + '*' -> skipBlockComment(currentContext) + else -> break + } + } + '#' -> skipLineComment(currentContext) + else -> break + } + } else { + break + } + } + + return currentContext + } + + /** + * ํ˜„์žฌ ์œ„์น˜์—์„œ EOF ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + override fun createEOFToken(context: LexingContext): Token { + return tokenFactory.createEOFToken(context.currentPosition) + } + + /** + * Lexer์˜ ํ˜„์žฌ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun getConfiguration(): Map = mapOf( + "debugMode" to debugMode, + "errorRecoveryMode" to errorRecoveryMode, + "characterRecognitionPolicy" to characterRecognitionPolicy.getConfiguration(), + "tokenValidationPolicy" to tokenValidationPolicy.getConfiguration(), + "inputValiditySpec" to inputValiditySpec.getConfiguration() + ) + + /** + * Lexer๋ฅผ ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun reset() { + resetStatistics() + debugMode = false + errorRecoveryMode = true + } + + /** + * ๋ถ„์„ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun getStatistics(): Map = statistics.toMap() + + /** + * ๋””๋ฒ„๊ทธ ๋ชจ๋“œ ์—ฌ๋ถ€๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun setDebugMode(enabled: Boolean) { + this.debugMode = enabled + } + + /** + * ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun setErrorRecoveryMode(enabled: Boolean) { + this.errorRecoveryMode = enabled + } + + /** + * ์ŠคํŠธ๋ฆฌ๋ฐ ๋ชจ๋“œ๋กœ ํ† ํฐ์„ ํ•˜๋‚˜์”ฉ ์ƒ์„ฑํ•˜๋Š” ์‹œํ€€์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun tokenizeAsSequence(input: String): Sequence = sequence { + var context = LexingContext.of(input) + + while (context.hasNext()) { + val (token, newContext) = nextToken(context) + if (token != null) { + yield(token) + } + context = newContext + } + + yield(createEOFToken(context)) + } + + /** + * ๋น„๋™๊ธฐ์ ์œผ๋กœ ์–ดํœ˜ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun tokenizeAsync(input: String, callback: (LexingResult) -> Unit) { + Thread { + try { + val result = tokenize(input) + callback(result) + } catch (e: Exception) { + val error = LexerException(ErrorCode.UNKNOWN_ERROR, message = "๋น„๋™๊ธฐ ๋ถ„์„ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", cause = e) + callback(LexingResult.failure(error)) + } + }.start() + } + + /** + * ๋ถ€๋ถ„ ์ž…๋ ฅ์— ๋Œ€ํ•œ ์ฆ๋ถ„ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun incrementalTokenize( + previousResult: LexingResult, + newInput: String, + changeStartIndex: Int + ): LexingResult { + // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„: ์ „์ฒด ์žฌ๋ถ„์„ + // ์‹ค์ œ๋กœ๋Š” ๋ณ€๊ฒฝ๋œ ๋ถ€๋ถ„๋งŒ ์žฌ๋ถ„์„ํ•˜๋Š” ์ตœ์ ํ™” ํ•„์š” + return tokenize(newInput) + } + + // Private helper methods + + private fun parseNumber(context: LexingContext): Pair { + val startPosition = context.currentPosition + val value = StringBuilder() + var currentContext = context + + while (currentContext.hasNext()) { + val char = currentContext.currentChar + if (char != null && characterRecognitionPolicy.isValidInNumber(char, value.isEmpty())) { + value.append(char) + currentContext = currentContext.advance() + } else { + break + } + } + + val token = tokenFactory.createNumberToken(value.toString(), startPosition) + return token to currentContext + } + + private fun parseIdentifier(context: LexingContext): Pair { + val startPosition = context.currentPosition + val value = StringBuilder() + var currentContext = context + + while (currentContext.hasNext()) { + val char = currentContext.currentChar + if (char != null && characterRecognitionPolicy.isIdentifierBody(char)) { + value.append(char) + currentContext = currentContext.advance() + } else { + break + } + } + + val token = tokenFactory.createIdentifierToken(value.toString(), startPosition) + return token to currentContext + } + + private fun parseVariable(context: LexingContext): Pair { + val startPosition = context.currentPosition + var currentContext = context.advance() // '{' ๊ฑด๋„ˆ๋›ฐ๊ธฐ + + val value = StringBuilder() + while (currentContext.hasNext()) { + val char = currentContext.currentChar + if (char == null) { + throw LexerException(ErrorCode.VALIDATION_FAILED, message = "๋ณ€์ˆ˜ ์ข…๋ฃŒ ์ „์— ์ž…๋ ฅ์ด ๋๋‚ฌ์Šต๋‹ˆ๋‹ค") + } else if (char == '}') { + currentContext = currentContext.advance() // '}' ๊ฑด๋„ˆ๋›ฐ๊ธฐ + break + } else if (characterRecognitionPolicy.isIdentifierBody(char)) { + value.append(char) + currentContext = currentContext.advance() + } else { + throw LexerException.unexpectedCharacter(char, currentContext.currentPosition.index) + } + } + + if (value.isEmpty()) { + throw LexerException(ErrorCode.VALIDATION_FAILED, message = "๋นˆ ๋ณ€์ˆ˜๋ช…์ž…๋‹ˆ๋‹ค") + } + + val token = tokenFactory.createVariableToken(value.toString(), startPosition) + return token to currentContext + } + + private fun parseOperator(context: LexingContext): Pair { + val startPosition = context.currentPosition + val currentChar = context.currentChar ?: throw LexerException(ErrorCode.VALIDATION_FAILED, message = "์—ฐ์‚ฐ์ž ํŒŒ์‹ฑ ์ค‘ ๋น„์–ด์žˆ๋Š” ๋ฌธ์ž") + var operator = currentChar.toString() + var currentContext = context.advance() + + // 2๋ฌธ์ž ์—ฐ์‚ฐ์ž ํ™•์ธ + if (currentContext.hasNext()) { + val nextChar = currentContext.currentChar + if (nextChar != null) { + val twoCharOperator = operator + nextChar + + if (tokenFactory.isOperator(twoCharOperator)) { + operator = twoCharOperator + currentContext = currentContext.advance() + } + } + } + + val token = tokenFactory.createOperatorToken(operator, startPosition) + return token to currentContext + } + + private fun parseDelimiter(context: LexingContext): Pair { + val startPosition = context.currentPosition + val delimiter = context.currentChar?.toString() ?: throw LexerException(ErrorCode.VALIDATION_FAILED, message = "๊ตฌ๋ถ„์ž ํŒŒ์‹ฑ ์ค‘ ๋น„์–ด์žˆ๋Š” ๋ฌธ์ž") + val currentContext = context.advance() + + val token = tokenFactory.createDelimiterToken(delimiter, startPosition) + return token to currentContext + } + + private fun skipLineComment(context: LexingContext): LexingContext { + var currentContext = context + + while (currentContext.hasNext()) { + val char = currentContext.currentChar + currentContext = currentContext.advance() + if (char == '\n') break + } + + return currentContext + } + + private fun skipBlockComment(context: LexingContext): LexingContext { + var currentContext = context.advance(2) // "/*" ๊ฑด๋„ˆ๋›ฐ๊ธฐ + + while (currentContext.hasNext()) { + val char = currentContext.currentChar + if (char == '*' && currentContext.peekChar() == '/') { + currentContext = currentContext.advance(2) // "*/" ๊ฑด๋„ˆ๋›ฐ๊ธฐ + break + } + currentContext = currentContext.advance() + } + + return currentContext + } + + private fun resetStatistics() { + statistics.clear() + statistics["tokensGenerated"] = 0 + statistics["inputsProcessed"] = 0 + statistics["errorsOccurred"] = 0 + statistics["totalProcessingTime"] = 0L + statistics["lastResetTime"] = System.currentTimeMillis() + } + + private fun updateStatistics(key: String, value: Any) { + statistics[key] = value + statistics["lastUpdatedTime"] = System.currentTimeMillis() + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/contract/LexerContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/contract/LexerContract.kt new file mode 100644 index 00000000..96b6b923 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/contract/LexerContract.kt @@ -0,0 +1,182 @@ +package hs.kr.entrydsm.domain.lexer.contract + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.lexer.values.LexingContext +import hs.kr.entrydsm.domain.lexer.values.LexingResult +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate + +/** + * Lexer ๋„๋ฉ”์ธ์˜ ํ•ต์‹ฌ ๊ณ„์•ฝ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ์ž…๋ ฅ ํ…์ŠคํŠธ๋ฅผ ํ† ํฐ ์ŠคํŠธ๋ฆผ์œผ๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๋ ‰์„œ์˜ ๊ธฐ๋ณธ ๊ณ„์•ฝ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์˜ ๋ชจ๋“  ํ† ํฐํ™” ๊ทœ์น™๊ณผ ์ •์ฑ…์„ ์บก์Аํ™”ํ•˜๋ฉฐ, + * ๋‹ค์–‘ํ•œ ๋ ‰์„œ ๊ตฌํ˜„์ฒด๋“ค์ด ์ค€์ˆ˜ํ•ด์•ผ ํ•  ํ‘œ์ค€์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Aggregate(context = "lexer") +interface LexerContract { + + /** + * ์ž…๋ ฅ ํ…์ŠคํŠธ๋ฅผ ํ† ํฐ ๋ฆฌ์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun tokenize(input: String): LexingResult + + /** + * ์ปจํ…์ŠคํŠธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์–ดํœ˜ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + fun tokenize(context: LexingContext): LexingResult + + /** + * ์ž…๋ ฅ ํ…์ŠคํŠธ์—์„œ ๋‹ค์Œ ํ† ํฐ ํ•˜๋‚˜๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + */ + fun nextToken(context: LexingContext): Pair + + /** + * ํ˜„์žฌ ์œ„์น˜์—์„œ ๋ฏธ๋ฆฌ ๋ณด๊ธฐ(lookahead) ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun peekTokens(context: LexingContext, count: Int): List + + /** + * ์ฃผ์–ด์ง„ ์ž…๋ ฅ์ด ์œ ํšจํ•œ ํ† ํฐ๋“ค๋กœ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + fun validate(input: String): Boolean + + /** + * ์ฃผ์–ด์ง„ ์ปจํ…์ŠคํŠธ๊ฐ€ ์œ ํšจํ•œ ์ƒํƒœ์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + fun validateContext(context: LexingContext): Boolean + + /** + * ํ˜„์žฌ ์œ„์น˜๊ฐ€ ํŠน์ • ํ† ํฐ ํƒ€์ž…์œผ๋กœ ์‹œ์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isTokenTypeAt(context: LexingContext, vararg expectedTypes: TokenType): Boolean + + /** + * ๊ณต๋ฐฑ ๋ฌธ์ž๋“ค์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋‹ค์Œ ์œ ํšจํ•œ ์œ„์น˜๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. + */ + fun skipWhitespace(context: LexingContext): LexingContext + + /** + * ์ฃผ์„์„ ๊ฑด๋„ˆ๋›ฐ๊ณ  ๋‹ค์Œ ์œ ํšจํ•œ ์œ„์น˜๋กœ ์ด๋™ํ•ฉ๋‹ˆ๋‹ค. + */ + fun skipComments(context: LexingContext): LexingContext + + /** + * ํ˜„์žฌ ์œ„์น˜์—์„œ EOF ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createEOFToken(context: LexingContext): Token + + /** + * Lexer์˜ ํ˜„์žฌ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getConfiguration(): Map + + /** + * Lexer๋ฅผ ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun reset() + + /** + * ๋ถ„์„ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getStatistics(): Map + + /** + * ๋””๋ฒ„๊ทธ ๋ชจ๋“œ ์—ฌ๋ถ€๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun setDebugMode(enabled: Boolean) + + /** + * ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun setErrorRecoveryMode(enabled: Boolean) + + /** + * ์ŠคํŠธ๋ฆฌ๋ฐ ๋ชจ๋“œ๋กœ ํ† ํฐ์„ ํ•˜๋‚˜์”ฉ ์ƒ์„ฑํ•˜๋Š” ์‹œํ€€์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun tokenizeAsSequence(input: String): Sequence + + /** + * ๋น„๋™๊ธฐ์ ์œผ๋กœ ์–ดํœ˜ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + fun tokenizeAsync(input: String, callback: (LexingResult) -> Unit) + + /** + * ๋ถ€๋ถ„ ์ž…๋ ฅ์— ๋Œ€ํ•œ ์ฆ๋ถ„ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + fun incrementalTokenize( + previousResult: LexingResult, + newInput: String, + changeStartIndex: Int + ): LexingResult +} + +/** + * ํ† ํฐํ™” ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @property inputLength ์ž…๋ ฅ ํ…์ŠคํŠธ ๊ธธ์ด + * @property tokenCount ์ƒ์„ฑ๋œ ํ† ํฐ ๊ฐœ์ˆ˜ + * @property processingTimeMs ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + * @property errorCount ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜ ๊ฐœ์ˆ˜ + * @property lastTokenizationTime ๋งˆ์ง€๋ง‰ ํ† ํฐํ™” ์‹œ๊ฐ„ + */ +data class TokenizationStats( + val inputLength: Int, + val tokenCount: Int, + val processingTimeMs: Long, + val errorCount: Int = 0, + val lastTokenizationTime: Long = System.currentTimeMillis() +) { + /** + * ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋œ ๋ฌธ์ž ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ดˆ๋‹น ๋ฌธ์ž ์ฒ˜๋ฆฌ๋Ÿ‰ + */ + fun getCharactersPerSecond(): Double = if (processingTimeMs > 0) { + (inputLength * 1000.0) / processingTimeMs + } else { + 0.0 + } + + /** + * ์ดˆ๋‹น ์ƒ์„ฑ๋œ ํ† ํฐ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ดˆ๋‹น ํ† ํฐ ์ƒ์„ฑ๋Ÿ‰ + */ + fun getTokensPerSecond(): Double = if (processingTimeMs > 0) { + (tokenCount * 1000.0) / processingTimeMs + } else { + 0.0 + } + + /** + * ํ‰๊ท  ํ† ํฐ ๊ธธ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ‰๊ท  ํ† ํฐ ๊ธธ์ด + */ + fun getAverageTokenLength(): Double = if (tokenCount > 0) { + inputLength.toDouble() / tokenCount + } else { + 0.0 + } +} + +/** + * ๋ ‰์„œ ์„ค์ •์„ ๋‹ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @property maxInputLength ์ตœ๋Œ€ ์ž…๋ ฅ ๊ธธ์ด + * @property strictMode ์—„๊ฒฉ ๋ชจ๋“œ (์˜ค๋ฅ˜์‹œ ์ฆ‰์‹œ ์ค‘๋‹จ) + * @property skipWhitespace ๊ณต๋ฐฑ ๋ฌธ์ž ๊ฑด๋„ˆ๋›ฐ๊ธฐ + * @property caseInsensitiveKeywords ํ‚ค์›Œ๋“œ ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์•ˆํ•จ + */ +data class LexerConfiguration( + val maxInputLength: Int = 10000, + val strictMode: Boolean = true, + val skipWhitespace: Boolean = true, + val caseInsensitiveKeywords: Boolean = true +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/Token.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/Token.kt new file mode 100644 index 00000000..08ac7f41 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/Token.kt @@ -0,0 +1,311 @@ +package hs.kr.entrydsm.domain.lexer.entities + +import hs.kr.entrydsm.domain.lexer.aggregates.LexerAggregate +import hs.kr.entrydsm.domain.lexer.exceptions.LexerException +import hs.kr.entrydsm.global.annotation.entities.Entity +import hs.kr.entrydsm.global.values.Position + +/** + * ํ† ํฐ์˜ ์ •๋ณด๋ฅผ ๋‹ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ๋ ‰์„œ๊ฐ€ ์ž…๋ ฅ ํ…์ŠคํŠธ๋ฅผ ๋ถ„์„ํ•˜์—ฌ ์ƒ์„ฑํ•˜๋Š” ํ† ํฐ์˜ ๋ชจ๋“  ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * ํ† ํฐ ํƒ€์ž…, ์›๋ณธ ๋ฌธ์ž์—ด ๊ฐ’, ์œ„์น˜ ์ •๋ณด๋ฅผ ๊ฐ€์ง€๋ฉฐ, ํŒŒ์„œ์—์„œ ๊ตฌ๋ฌธ ๋ถ„์„์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋˜์–ด ์•ˆ์ „ํ•œ ํ† ํฐ ์ „๋‹ฌ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @property type ํ† ํฐ์˜ ํƒ€์ž… + * @property value ํ† ํฐ์˜ ์›๋ณธ ๋ฌธ์ž์—ด ๊ฐ’ + * @property position ํ† ํฐ์˜ ์œ„์น˜ ์ •๋ณด + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(context = "lexer", aggregateRoot = LexerAggregate::class) +data class Token( + val type: TokenType, + val value: String, + val position: Position +) { + + init { + if (value.isEmpty() && type != TokenType.DOLLAR) { + throw LexerException.tokenValueEmptyExceptEof(type.name) + } + // Position์€ length ์†์„ฑ์ด ์—†์œผ๋ฏ€๋กœ ๊ฒ€์ฆ ์ œ๊ฑฐ + } + + companion object { + /** + * ๊ธฐ๋ณธ ์œ„์น˜ ์ •๋ณด๋กœ ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @param value ํ† ํฐ ๊ฐ’ + * @param startIndex ์‹œ์ž‘ ์ธ๋ฑ์Šค (๊ธฐ๋ณธ๊ฐ’: 0) + * @return Token ์ธ์Šคํ„ด์Šค + */ + fun of(type: TokenType, value: String, startIndex: Int = 0): Token { + val position = Position.of(startIndex) + return Token(type, value, position) + } + + /** + * Position์„ ์ด์šฉํ•˜์—ฌ ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @param value ํ† ํฐ ๊ฐ’ + * @param start ์‹œ์ž‘ ์œ„์น˜ + * @return Token ์ธ์Šคํ„ด์Šค + */ + fun at(type: TokenType, value: String, start: Position): Token { + val position = start + return Token(type, value, position) + } + + /** + * ๋ฒ”์œ„๋ฅผ ์ง€์ •ํ•˜์—ฌ ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @param value ํ† ํฐ ๊ฐ’ + * @param startIndex ์‹œ์ž‘ ์ธ๋ฑ์Šค + * @param endIndex ๋ ์ธ๋ฑ์Šค + * @return Token ์ธ์Šคํ„ด์Šค + */ + fun between(type: TokenType, value: String, startIndex: Int, endIndex: Int): Token { + val position = Position.of(startIndex) + return Token(type, value, position) + } + + /** + * EOF ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param position EOF ์œ„์น˜ (๊ธฐ๋ณธ๊ฐ’: Position.START) + * @return EOF Token ์ธ์Šคํ„ด์Šค + */ + fun eof(position: Position = Position.START): Token { + return Token(TokenType.DOLLAR, "$", position) + } + + /** + * ์ˆซ์ž ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ˆซ์ž ๋ฌธ์ž์—ด + * @param startIndex ์‹œ์ž‘ ์ธ๋ฑ์Šค + * @return ์ˆซ์ž Token ์ธ์Šคํ„ด์Šค + */ + fun number(value: String, startIndex: Int): Token { + if (!value.matches(Regex("""\d+(\.\d+)?"""))) { + throw LexerException.invalidNumberFormat(value) + } + return of(TokenType.NUMBER, value, startIndex) + } + + /** + * ์‹๋ณ„์ž ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์‹๋ณ„์ž ๋ฌธ์ž์—ด + * @param startIndex ์‹œ์ž‘ ์ธ๋ฑ์Šค + * @return ์‹๋ณ„์ž Token ์ธ์Šคํ„ด์Šค + */ + fun identifier(value: String, startIndex: Int): Token { + if (!value.matches(Regex("""[a-zA-Z_][a-zA-Z0-9_]*"""))) { + throw LexerException.invalidIdentifierFormat(value) + } + + // ํ‚ค์›Œ๋“œ ๊ฒ€์‚ฌ + val keywordType = TokenType.findKeyword(value) + val type = keywordType ?: TokenType.IDENTIFIER + return of(type, value, startIndex) + } + + /** + * ๋ณ€์ˆ˜ ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๋ณ€์ˆ˜๋ช… (์ค‘๊ด„ํ˜ธ ์ œ์™ธ) + * @param startIndex ์‹œ์ž‘ ์ธ๋ฑ์Šค (์ค‘๊ด„ํ˜ธ ํฌํ•จ) + * @return ๋ณ€์ˆ˜ Token ์ธ์Šคํ„ด์Šค + */ + fun variable(value: String, startIndex: Int): Token { + if (value.isEmpty()) { + throw LexerException.variableNameEmpty(value) + } + + val position = Position.of(startIndex) // {๋ณ€์ˆ˜๋ช…} ํฌํ•จ + return Token(TokenType.VARIABLE, value, position) + } + + /** + * ์—ฐ์‚ฐ์ž ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ์—ฐ์‚ฐ์ž ํƒ€์ž… + * @param value ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @param startIndex ์‹œ์ž‘ ์ธ๋ฑ์Šค + * @return ์—ฐ์‚ฐ์ž Token ์ธ์Šคํ„ด์Šค + */ + fun operator(type: TokenType, value: String, startIndex: Int): Token { + if (!type.isOperator) { + throw LexerException.notOperatorType(type.name) + } + + return of(type, value, startIndex) + } + } + + /** + * ํ† ํฐ์ด ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isTerminal(): Boolean = type.isTerminal + + /** + * ํ† ํฐ์ด ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isNonTerminal(): Boolean = type.isNonTerminal() + + /** + * ํ† ํฐ์ด ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฐ์‚ฐ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isOperator(): Boolean = type.isOperator + + /** + * ํ† ํฐ์ด ํ‚ค์›Œ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ‚ค์›Œ๋“œ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isKeyword(): Boolean = type.isKeyword + + /** + * ํ† ํฐ์ด ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฆฌํ„ฐ๋Ÿด์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isLiteral(): Boolean = type.isLiteral + + /** + * ํ† ํฐ์ด ์ˆซ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆซ์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isNumber(): Boolean = type == TokenType.NUMBER + + /** + * ํ† ํฐ์ด ์‹๋ณ„์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹๋ณ„์ž์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isIdentifier(): Boolean = type == TokenType.IDENTIFIER + + /** + * ํ† ํฐ์ด ๋ณ€์ˆ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€์ˆ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isVariable(): Boolean = type == TokenType.VARIABLE + + /** + * ํ† ํฐ์ด EOF์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return EOF์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isEOF(): Boolean = type == TokenType.DOLLAR + + /** + * ํ† ํฐ์ด ๋ถˆ๋ฆฐ ๊ฐ’์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ถˆ๋ฆฐ ๊ฐ’์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isBoolean(): Boolean = type.isBooleanLiteral() + + /** + * ํ† ํฐ ๊ฐ’์„ ์ˆซ์ž๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€ํ™˜๋œ Double ๊ฐ’ + * @throws IllegalStateException ์ˆซ์ž ํ† ํฐ์ด ์•„๋‹Œ ๊ฒฝ์šฐ + * @throws NumberFormatException ์ˆซ์ž ๋ณ€ํ™˜ ์‹คํŒจ์‹œ + */ + fun toNumber(): Double { + if (!isNumber()) { + throw LexerException.notNumberToken(type.name) + } + + return value.toDouble() + } + + /** + * ํ† ํฐ ๊ฐ’์„ ๋ถˆ๋ฆฐ์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ€ํ™˜๋œ Boolean ๊ฐ’ + * @throws IllegalStateException ๋ถˆ๋ฆฐ ํ† ํฐ์ด ์•„๋‹Œ ๊ฒฝ์šฐ + */ + fun toBoolean(): Boolean { + if (!isBoolean()) { + throw LexerException.notBooleanToken(type.name) + } + + return when (type) { + TokenType.TRUE -> true + TokenType.FALSE -> false + else -> throw LexerException.unexpectedBooleanTokenType(type.name) + } + } + + /** + * ํ† ํฐ์˜ ์‹œ์ž‘ ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹œ์ž‘ Position + */ + fun getStartPosition(): Position = position + + /** + * ํ† ํฐ์˜ ๋ ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ Position + */ + fun getEndPosition(): Position = position.advance(value.length) + + /** + * ํ† ํฐ์˜ ๊ธธ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ† ํฐ ๊ธธ์ด + */ + fun getLength(): Int = value.length + + /** + * ํ† ํฐ์ด ํŠน์ • ์œ„์น˜๋ฅผ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param checkPosition ํ™•์ธํ•  ์œ„์น˜ + * @return ํฌํ•จํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun contains(checkPosition: Position): Boolean = checkPosition.index >= position.index && checkPosition.index < position.index + value.length + + /** + * ํ† ํฐ์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ† ํฐ ์นดํ…Œ๊ณ ๋ฆฌ ๋ฌธ์ž์—ด + */ + fun getCategory(): String = type.getCategory() + + /** + * ํ† ํฐ์„ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ’์ด ์žˆ์œผ๋ฉด "TYPE(value)", ์—†์œผ๋ฉด "TYPE" ํ˜•ํƒœ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ† ํฐ ๋ฌธ์ž์—ด ํ‘œํ˜„ + */ + override fun toString(): String = if (value.isNotEmpty() && type != TokenType.DOLLAR) { + "$type($value)" + } else { + type.toString() + } + + /** + * ๋””๋ฒ„๊น…์šฉ ์ƒ์„ธ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "TYPE(value) at position" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toDetailString(): String = "$this at ${position.toShortString()}" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/TokenPosition.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/TokenPosition.kt new file mode 100644 index 00000000..fd5461f5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/TokenPosition.kt @@ -0,0 +1,202 @@ +package hs.kr.entrydsm.domain.lexer.entities + +import hs.kr.entrydsm.domain.lexer.aggregates.LexerAggregate +import hs.kr.entrydsm.domain.lexer.exceptions.LexerException +import hs.kr.entrydsm.global.annotation.entities.Entity +import hs.kr.entrydsm.global.values.Position + +/** + * ํ† ํฐ์˜ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * Lexer ๋„๋ฉ”์ธ์ด ์†Œ์œ ํ•˜๋Š” ํ† ํฐ ํŠนํ™” ์œ„์น˜ ์ •๋ณด๋กœ, ๊ธฐ๋ณธ Position์„ ํ™•์žฅํ•˜์—ฌ + * ํ† ํฐ์˜ ์‹œ์ž‘๊ณผ ๋ ์œ„์น˜, ๊ธธ์ด ๋“ฑ์˜ ์ถ”๊ฐ€ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * ์˜ค๋ฅ˜ ๋ณด๊ณ ์™€ ๋””๋ฒ„๊น…์—์„œ ์ •ํ™•ํ•œ ํ† ํฐ ์œ„์น˜๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ฐ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @property start ํ† ํฐ ์‹œ์ž‘ ์œ„์น˜ + * @property end ํ† ํฐ ๋ ์œ„์น˜ + * @property length ํ† ํฐ ๊ธธ์ด + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(context = "lexer", aggregateRoot = LexerAggregate::class) +data class TokenPosition( + val start: Position, + val end: Position, + val length: Int = end.index - start.index +) { + + init { + if (start.index > end.index) { + throw LexerException.invalidPositionOrder(start.index, end.index) + } + + if (length < 0) { + throw LexerException.negativeTokenLength(length) + } + } + + companion object { + /** + * ๋‹จ์ผ ์œ„์น˜์—์„œ ํ† ํฐ ์œ„์น˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param position ํ† ํฐ ์œ„์น˜ + * @param length ํ† ํฐ ๊ธธ์ด (๊ธฐ๋ณธ๊ฐ’: 1) + * @return TokenPosition ์ธ์Šคํ„ด์Šค + */ + fun at(position: Position, length: Int = 1): TokenPosition { + val endPosition = position.advance(length) + return TokenPosition(position, endPosition, length) + } + + /** + * ์ธ๋ฑ์Šค์™€ ๊ธธ์ด๋กœ ํ† ํฐ ์œ„์น˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startIndex ์‹œ์ž‘ ์ธ๋ฑ์Šค + * @param length ํ† ํฐ ๊ธธ์ด + * @return TokenPosition ์ธ์Šคํ„ด์Šค + */ + fun of(startIndex: Int, length: Int): TokenPosition { + val start = Position.of(startIndex) + val end = Position.of(startIndex + length) + return TokenPosition(start, end, length) + } + + /** + * ์‹œ์ž‘๊ณผ ๋ ์ธ๋ฑ์Šค๋กœ ํ† ํฐ ์œ„์น˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startIndex ์‹œ์ž‘ ์ธ๋ฑ์Šค + * @param endIndex ๋ ์ธ๋ฑ์Šค + * @return TokenPosition ์ธ์Šคํ„ด์Šค + */ + fun between(startIndex: Int, endIndex: Int): TokenPosition { + val start = Position.of(startIndex) + val end = Position.of(endIndex) + return TokenPosition(start, end) + } + + /** + * ํ…์ŠคํŠธ์™€ ์œ„์น˜ ์ •๋ณด๋กœ ์ •ํ™•ํ•œ ํ† ํฐ ์œ„์น˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ์ „์ฒด ํ…์ŠคํŠธ + * @param startIndex ํ† ํฐ ์‹œ์ž‘ ์ธ๋ฑ์Šค + * @param endIndex ํ† ํฐ ๋ ์ธ๋ฑ์Šค + * @return ๊ณ„์‚ฐ๋œ TokenPosition ์ธ์Šคํ„ด์Šค + */ + fun calculate(text: String, startIndex: Int, endIndex: Int): TokenPosition { + val start = Position.calculate(text, startIndex) + val end = Position.calculate(text, endIndex) + return TokenPosition(start, end) + } + } + + /** + * ํ† ํฐ์ด ํŠน์ • ์œ„์น˜๋ฅผ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param position ํ™•์ธํ•  ์œ„์น˜ + * @return ํฌํ•จํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun contains(position: Position): Boolean = + position.index >= start.index && position.index < end.index + + /** + * ํ† ํฐ์ด ํŠน์ • ์ธ๋ฑ์Šค๋ฅผ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ํ™•์ธํ•  ์ธ๋ฑ์Šค + * @return ํฌํ•จํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun contains(index: Int): Boolean = index >= start.index && index < end.index + + /** + * ๋‹ค๋ฅธ ํ† ํฐ ์œ„์น˜์™€ ๊ฒน์น˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ํ† ํฐ ์œ„์น˜ + * @return ๊ฒน์น˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun overlaps(other: TokenPosition): Boolean = + start.index < other.end.index && end.index > other.start.index + + /** + * ๋‹ค๋ฅธ ํ† ํฐ ์œ„์น˜์™€ ์ธ์ ‘ํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ํ† ํฐ ์œ„์น˜ + * @return ์ธ์ ‘ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAdjacentTo(other: TokenPosition): Boolean = + end.index == other.start.index || start.index == other.end.index + + /** + * ๋‹ค๋ฅธ ํ† ํฐ ์œ„์น˜ ์ด์ „์— ์œ„์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ํ† ํฐ ์œ„์น˜ + * @return ์ด์ „์— ์œ„์น˜ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isBefore(other: TokenPosition): Boolean = end.index <= other.start.index + + /** + * ๋‹ค๋ฅธ ํ† ํฐ ์œ„์น˜ ์ดํ›„์— ์œ„์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ํ† ํฐ ์œ„์น˜ + * @return ์ดํ›„์— ์œ„์น˜ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAfter(other: TokenPosition): Boolean = start.index >= other.end.index + + /** + * ํ† ํฐ ์œ„์น˜๋ฅผ ํ™•์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param additionalLength ์ถ”๊ฐ€ํ•  ๊ธธ์ด + * @return ํ™•์žฅ๋œ TokenPosition ์ธ์Šคํ„ด์Šค + */ + fun extend(additionalLength: Int): TokenPosition { + if (additionalLength < 0) { + throw LexerException.negativeAdditionalLength(additionalLength) + } + + val newEnd = end.advance(additionalLength) + return TokenPosition(start, newEnd, length + additionalLength) + } + + /** + * ํ† ํฐ์ด ํ•œ ์ค„์— ์œ„์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•œ ์ค„์— ์œ„์น˜ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSingleLine(): Boolean = start.line == end.line + + /** + * ํ† ํฐ์ด ์—ฌ๋Ÿฌ ์ค„์— ๊ฑธ์ณ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฌ๋Ÿฌ ์ค„์— ๊ฑธ์ณ ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isMultiLine(): Boolean = !isSingleLine() + + /** + * ํ† ํฐ์ด ์ฐจ์ง€ํ•˜๋Š” ์ค„ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ค„ ์ˆ˜ + */ + fun getLineCount(): Int = end.line - start.line + 1 + + /** + * ํ† ํฐ์˜ ๋ฒ”์œ„๋ฅผ "start-end" ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฒ”์œ„ ๋ฌธ์ž์—ด + */ + fun toRangeString(): String = "${start.toShortString()}-${end.toShortString()}" + + /** + * ํ† ํฐ ์œ„์น˜ ์ •๋ณด๋ฅผ ์ƒ์„ธ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "start-end (length)" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + override fun toString(): String = "${toRangeString()} (length: $length)" + + /** + * ๊ฐ„๋‹จํ•œ ํ˜•ํƒœ์˜ ํ† ํฐ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "line:column" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toShortString(): String = start.toShortString() +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/TokenType.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/TokenType.kt new file mode 100644 index 00000000..ecd165b7 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/TokenType.kt @@ -0,0 +1,249 @@ +package hs.kr.entrydsm.domain.lexer.entities + +import hs.kr.entrydsm.domain.lexer.aggregates.LexerAggregate +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * POC ์ฝ”๋“œ์™€ ์™„์ „ํžˆ ์ผ์น˜ํ•˜๋Š” ํ† ํฐ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + * + * POC ์ฝ”๋“œ์˜ ์™„์ „ํ•œ LR(1) ํŒŒ์„œ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๊ณผ ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์„ + * ์ •ํ™•ํžˆ ๋ณต์ œํ•˜์—ฌ ๊ธฐ๋Šฅ์  ๋ˆ„๋ฝ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. POC์˜ 34๊ฐœ ์ƒ์„ฑ ๊ทœ์น™๊ณผ + * ์™„์ „ํžˆ ํ˜ธํ™˜๋ฉ๋‹ˆ๋‹ค. + * + * @see POC Grammar object์˜ TokenType ์ •์˜ + * + * @author kangeunchan + * @since 2025.07.28 + */ +@Entity(context = "lexer", aggregateRoot = LexerAggregate::class) +enum class TokenType( + val isTerminal: Boolean = true, + val isOperator: Boolean = false, + val isKeyword: Boolean = false, + val isLiteral: Boolean = false, + val symbol: String? = null +) { + // === POC ์ฝ”๋“œ์˜ ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค (์ •ํ™•ํ•œ ๋ณต์ œ) === + NUMBER(isLiteral = true), // ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด (123, 3.14) + IDENTIFIER(isLiteral = true), // ์‹๋ณ„์ž (๋ณ€์ˆ˜๋ช…, ํ•จ์ˆ˜๋ช…) + VARIABLE(isLiteral = true), // ๋ณ€์ˆ˜ ํ† ํฐ + + // POC ์ฝ”๋“œ์˜ ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž๋“ค + PLUS(isOperator = true, symbol = "+"), // + + MINUS(isOperator = true, symbol = "-"), // - + MULTIPLY(isOperator = true, symbol = "*"), // * + DIVIDE(isOperator = true, symbol = "/"), // / + POWER(isOperator = true, symbol = "^"), // ^ + MODULO(isOperator = true, symbol = "%"), // % + + // POC ์ฝ”๋“œ์˜ ๋น„๊ต ์—ฐ์‚ฐ์ž๋“ค + EQUAL(isOperator = true, symbol = "=="), // == + NOT_EQUAL(isOperator = true, symbol = "!="), // != + LESS(isOperator = true, symbol = "<"), // < + LESS_EQUAL(isOperator = true, symbol = "<="), // <= + GREATER(isOperator = true, symbol = ">"), // > + GREATER_EQUAL(isOperator = true, symbol = ">="), // >= + + // ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค - ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž + AND(isOperator = true, symbol = "&&"), // ๋…ผ๋ฆฌ AND (&&) + OR(isOperator = true, symbol = "||"), // ๋…ผ๋ฆฌ OR (||) + NOT(isOperator = true, symbol = "!"), // ๋…ผ๋ฆฌ NOT (!) + + // ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค - ๊ตฌ๋ถ„์ž + LEFT_PAREN(symbol = "("), // ์™ผ์ชฝ ๊ด„ํ˜ธ (() + RIGHT_PAREN(symbol = ")"), // ์˜ค๋ฅธ์ชฝ ๊ด„ํ˜ธ ()) + COMMA(symbol = ","), // ์‰ผํ‘œ (,) + + // ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค - ํ‚ค์›Œ๋“œ + IF(isKeyword = true, symbol = "if"), // IF ํ‚ค์›Œ๋“œ + TRUE(isKeyword = true, isLiteral = true, symbol = "true"), // TRUE ํ‚ค์›Œ๋“œ + FALSE(isKeyword = true, isLiteral = true, symbol = "false"), // FALSE ํ‚ค์›Œ๋“œ + BOOLEAN(isKeyword = true, isLiteral = true, symbol = "boolean"), // BOOLEAN ํƒ€์ž… + FUNCTION(isKeyword = true, symbol = "function"), // FUNCTION ํ‚ค์›Œ๋“œ + + // ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค - ์ถ”๊ฐ€ ๊ตฌ๋ถ„์ž + QUESTION(symbol = "?"), // ๋ฌผ์Œํ‘œ (?) + COLON(symbol = ":"), // ์ฝœ๋ก  (:) + WHITESPACE, // ๊ณต๋ฐฑ + + // ์ถ”๊ฐ€ ์—ฐ์‚ฐ์ž ๋ณ„์นญ + LESS_THAN(isOperator = true, symbol = "<"), // < (LESS์˜ ๋ณ„์นญ) + GREATER_THAN(isOperator = true, symbol = ">"), // > (GREATER์˜ ๋ณ„์นญ) + + // ํŠน์ˆ˜ ์‹ฌ๋ณผ + DOLLAR(symbol = "$"), // EOF (End Of File) ์‹ฌ๋ณผ + EPSILON, // ์—ก์‹ค๋ก  (๋นˆ ๋ฌธ์ž์—ด) ์‹ฌ๋ณผ + + // ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค (ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ์ƒ์„ฑ๋˜๋Š” ์ค‘๊ฐ„ ์‹ฌ๋ณผ) + START(isTerminal = false), // ๋ฌธ๋ฒ•์˜ ์‹œ์ž‘ ์‹ฌ๋ณผ (ํ™•์žฅ๋œ ๋ฌธ๋ฒ•์šฉ) + EXPR(isTerminal = false), // ํ‘œํ˜„์‹ + AND_EXPR(isTerminal = false), // AND ํ‘œํ˜„์‹ + COMP_EXPR(isTerminal = false), // ๋น„๊ต ํ‘œํ˜„์‹ + ARITH_EXPR(isTerminal = false), // ์‚ฐ์ˆ  ํ‘œํ˜„์‹ + TERM(isTerminal = false), // ํ•ญ + FACTOR(isTerminal = false), // ์ธ์ž + PRIMARY(isTerminal = false), // ๊ธฐ๋ณธ ์š”์†Œ + ARGS(isTerminal = false), // ํ•จ์ˆ˜ ์ธ์ˆ˜ ๋ชฉ๋ก + + // ์ถ”๊ฐ€ ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค + EQUALITY_EXPR(isTerminal = false), // ๋™๋“ฑ์„ฑ ํ‘œํ˜„์‹ + RELATIONAL_EXPR(isTerminal = false), // ๊ด€๊ณ„ ํ‘œํ˜„์‹ + ADDITIVE_EXPR(isTerminal = false), // ๋ง์…ˆ/๋บ„์…ˆ ํ‘œํ˜„์‹ + MULTIPLICATIVE_EXPR(isTerminal = false), // ๊ณฑ์…ˆ/๋‚˜๋ˆ—์…ˆ ํ‘œํ˜„์‹ + UNARY_EXPR(isTerminal = false), // ๋‹จํ•ญ ํ‘œํ˜„์‹ + POWER_EXPR(isTerminal = false), // ๊ฑฐ๋“ญ์ œ๊ณฑ ํ‘œํ˜„์‹ + PRIMARY_EXPR(isTerminal = false), // ๊ธฐ๋ณธ ํ‘œํ˜„์‹ + ATOM(isTerminal = false), // ์›์ž ํ‘œํ˜„์‹ + FUNCTION_CALL(isTerminal = false), // ํ•จ์ˆ˜ ํ˜ธ์ถœ + ARGUMENTS(isTerminal = false), // ์ธ์ˆ˜๋“ค + ARGUMENT_LIST(isTerminal = false), // ์ธ์ˆ˜ ๋ชฉ๋ก + CONDITIONAL_EXPR(isTerminal = false); // ์กฐ๊ฑด๋ถ€ ํ‘œํ˜„์‹ + + /** + * ํ† ํฐ ํƒ€์ž…์ด ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isNonTerminal(): Boolean = !isTerminal + + /** + * ํ† ํฐ ํƒ€์ž…์ด ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isUnaryOperator(): Boolean = this in unaryOperators + + /** + * ํ† ํฐ ํƒ€์ž…์ด ์ดํ•ญ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isBinaryOperator(): Boolean = isOperator && !isUnaryOperator() + + /** + * ํ† ํฐ ํƒ€์ž…์ด ๋น„๊ต ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isComparisonOperator(): Boolean = this in comparisonOperators + + /** + * ํ† ํฐ ํƒ€์ž…์ด ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isArithmeticOperator(): Boolean = this in arithmeticOperators + + /** + * ํ† ํฐ ํƒ€์ž…์ด ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isLogicalOperator(): Boolean = this in logicalOperators + + /** + * ํ† ํฐ ํƒ€์ž…์ด ๋ถˆ๋ฆฐ ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isBooleanLiteral(): Boolean = this in booleanLiterals + + /** + * ํ† ํฐ ํƒ€์ž…์ด ๊ด„ํ˜ธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isParenthesis(): Boolean = this in parentheses + + /** + * ํ† ํฐ ํƒ€์ž…์ด ์—ฌ๋Š” ๊ด„ํ˜ธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isOpeningParenthesis(): Boolean = this == LEFT_PAREN + + /** + * ํ† ํฐ ํƒ€์ž…์ด ๋‹ซ๋Š” ๊ด„ํ˜ธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isClosingParenthesis(): Boolean = this == RIGHT_PAREN + + /** + * ํ† ํฐ ํƒ€์ž…์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getCategory(): String = when { + isKeyword -> CategoryNames.KEYWORD + isLiteral && !isKeyword -> CategoryNames.LITERAL + isOperator -> CategoryNames.OPERATOR + isParenthesis() -> CategoryNames.PARENTHESIS + this == COMMA -> CategoryNames.SEPARATOR + this == DOLLAR -> CategoryNames.EOF + isNonTerminal() -> CategoryNames.NON_TERMINAL + else -> CategoryNames.UNKNOWN + } + + companion object { + // ์—ฐ์‚ฐ์ž ๊ทธ๋ฃน๋“ค - ํ•˜๋“œ์ฝ”๋”ฉ ์ตœ์†Œํ™”๋ฅผ ์œ„ํ•œ ์ง‘ํ•ฉ ์ •์˜ + private val unaryOperators by lazy { + values().filter { it.isOperator && it.symbol in setOf("-", "!") }.toSet() + } + + private val comparisonOperators by lazy { + values().filter { it.isOperator && it.symbol?.let { s -> + s in setOf("==", "!=", "<", "<=", ">", ">=") + } == true }.toSet() + } + + private val arithmeticOperators by lazy { + values().filter { it.isOperator && it.symbol?.let { s -> + s in setOf("+", "-", "*", "/", "^", "%") + } == true }.toSet() + } + + private val logicalOperators by lazy { + values().filter { it.isOperator && it.symbol?.let { s -> + s in setOf("&&", "||", "!") + } == true }.toSet() + } + + private val booleanLiterals by lazy { + values().filter { it.symbol in setOf("true", "false") }.toSet() + } + + private val parentheses by lazy { + values().filter { it.symbol in setOf("(", ")") }.toSet() + } + + // ํ‚ค์›Œ๋“œ ๋งต - ๋™์  ์ƒ์„ฑ + private val keywordMap by lazy { + values().filter { it.isKeyword && it.symbol != null } + .associateBy { it.symbol!! } + } + + /** + * ๋ชจ๋“  ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getTerminals(): List = values().filter { it.isTerminal } + + /** + * ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getNonTerminals(): List = values().filter { it.isNonTerminal() } + + /** + * ๋ชจ๋“  ์—ฐ์‚ฐ์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getOperators(): List = values().filter { it.isOperator } + + /** + * ๋ชจ๋“  ํ‚ค์›Œ๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getKeywords(): List = values().filter { it.isKeyword } + + /** + * ๋ฌธ์ž์—ด๋กœ๋ถ€ํ„ฐ ํ‚ค์›Œ๋“œ ํ† ํฐ ํƒ€์ž…์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + fun findKeyword(text: String): TokenType? = keywordMap[text.lowercase()] + + /** + * ์‹ฌ๋ณผ๋กœ๋ถ€ํ„ฐ ํ† ํฐ ํƒ€์ž…์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + fun findBySymbol(symbol: String): TokenType? = + values().find { it.symbol == symbol } + } + + /** + * ์นดํ…Œ๊ณ ๋ฆฌ ์ด๋ฆ„๋“ค์„ ์ƒ์ˆ˜๋กœ ๊ด€๋ฆฌ + */ + private object CategoryNames { + const val KEYWORD = "KEYWORD" + const val LITERAL = "LITERAL" + const val OPERATOR = "OPERATOR" + const val PARENTHESIS = "PARENTHESIS" + const val SEPARATOR = "SEPARATOR" + const val EOF = "EOF" + const val NON_TERMINAL = "NON_TERMINAL" + const val UNKNOWN = "UNKNOWN" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/TokenTypePOC.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/TokenTypePOC.kt new file mode 100644 index 00000000..ecca3168 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/entities/TokenTypePOC.kt @@ -0,0 +1,179 @@ +package hs.kr.entrydsm.domain.lexer.entities + +import hs.kr.entrydsm.domain.lexer.aggregates.LexerAggregate +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * POC ์ฝ”๋“œ์™€ ์™„์ „ํžˆ ์ผ์น˜ํ•˜๋Š” ํ† ํฐ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + * + * POC ์ฝ”๋“œ์˜ Grammar ๊ฐ์ฒด์—์„œ ์ •์˜๋œ ์ •ํ™•ํ•œ ํ† ํฐ ํƒ€์ž…๋“ค์„ ๋ณต์ œํ•˜์—ฌ + * 34๊ฐœ ์ƒ์„ฑ ๊ทœ์น™๊ณผ ์™„์ „ํžˆ ํ˜ธํ™˜๋˜๋„๋ก ๊ตฌํ˜„ํ–ˆ์Šต๋‹ˆ๋‹ค. + * ๊ธฐ๋Šฅ์  ๋ˆ„๋ฝ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด POC์™€ 1:1 ๋Œ€์‘๋ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.28 + */ +@Entity(context = "lexer", aggregateRoot = LexerAggregate::class) +enum class TokenTypePOC { + // === POC ์ฝ”๋“œ์˜ ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค (์ •ํ™•ํ•œ ๋ณต์ œ) === + + // ๋ฆฌํ„ฐ๋Ÿด๋“ค + NUMBER, // ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด (123, 3.14) + IDENTIFIER, // ์‹๋ณ„์ž (๋ณ€์ˆ˜๋ช…, ํ•จ์ˆ˜๋ช…) + VARIABLE, // ๋ณ€์ˆ˜ ํ† ํฐ + + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž๋“ค + PLUS, // + + MINUS, // - + MULTIPLY, // * + DIVIDE, // / + POWER, // ^ + MODULO, // % + + // ๋น„๊ต ์—ฐ์‚ฐ์ž๋“ค + EQUAL, // == + NOT_EQUAL, // != + LESS, // < + LESS_EQUAL, // <= + GREATER, // > + GREATER_EQUAL, // >= + + // ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž๋“ค + AND, // && + OR, // || + NOT, // ! + + // ๊ตฌ๋ถ„์ž๋“ค + LEFT_PAREN, // ( + RIGHT_PAREN, // ) + COMMA, // , + + // ํ‚ค์›Œ๋“œ๋“ค + IF, // if + TRUE, // true + FALSE, // false + + // ํŠน์ˆ˜ ํ† ํฐ + DOLLAR, // $ (ํŒŒ์„œ ์ข…๋ฃŒ ๋งˆ์ปค) + + // === POC ์ฝ”๋“œ์˜ ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค (์ •ํ™•ํ•œ ๋ณต์ œ) === + + START, // ํ™•์žฅ๋œ ์‹œ์ž‘ ์‹ฌ๋ณผ + EXPR, // ํ‘œํ˜„์‹ (์ตœ์ƒ์œ„) + AND_EXPR, // ๋…ผ๋ฆฌ๊ณฑ ํ‘œํ˜„์‹ + COMP_EXPR, // ๋น„๊ต ํ‘œํ˜„์‹ + ARITH_EXPR, // ์‚ฐ์ˆ  ํ‘œํ˜„์‹ + TERM, // ํ•ญ (๊ณฑ์…ˆ/๋‚˜๋ˆ—์…ˆ ๋ ˆ๋ฒจ) + FACTOR, // ์ธ์ˆ˜ (๊ฑฐ๋“ญ์ œ๊ณฑ ๋ ˆ๋ฒจ) + PRIMARY, // ๊ธฐ๋ณธ ์š”์†Œ (๊ด„ํ˜ธ, ๋ฆฌํ„ฐ๋Ÿด ๋“ฑ) + ARGS; // ํ•จ์ˆ˜ ์ธ์ˆ˜ ๋ชฉ๋ก + + /** + * ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ terminals ์ง‘ํ•ฉ๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun isTerminal(): Boolean { + return ordinal <= DOLLAR.ordinal + } + + /** + * ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ nonTerminals ์ง‘ํ•ฉ๊ณผ ์ผ์น˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun isNonTerminal(): Boolean { + return ordinal > DOLLAR.ordinal + } + + /** + * ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isOperator(): Boolean { + return this in setOf( + PLUS, MINUS, MULTIPLY, DIVIDE, POWER, MODULO, + EQUAL, NOT_EQUAL, LESS, LESS_EQUAL, GREATER, GREATER_EQUAL, + AND, OR, NOT + ) + } + + /** + * ํ‚ค์›Œ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isKeyword(): Boolean { + return this in setOf(IF, TRUE, FALSE) + } + + /** + * ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isLiteral(): Boolean { + return this in setOf(NUMBER, IDENTIFIER, VARIABLE, TRUE, FALSE) + } + + companion object { + /** + * POC ์ฝ”๋“œ์˜ terminals ์ง‘ํ•ฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getTerminals(): Set { + return values().filter { it.isTerminal() }.toSet() + } + + /** + * POC ์ฝ”๋“œ์˜ nonTerminals ์ง‘ํ•ฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getNonTerminals(): Set { + return values().filter { it.isNonTerminal() }.toSet() + } + + /** + * ์—ฐ์‚ฐ์ž๋“ค์˜ ์ง‘ํ•ฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getOperators(): Set { + return values().filter { it.isOperator() }.toSet() + } + + /** + * ํ‚ค์›Œ๋“œ๋“ค์˜ ์ง‘ํ•ฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getKeywords(): Set { + return values().filter { it.isKeyword() }.toSet() + } + + /** + * ๋ฆฌํ„ฐ๋Ÿด๋“ค์˜ ์ง‘ํ•ฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getLiterals(): Set { + return values().filter { it.isLiteral() }.toSet() + } + + /** + * POC ์ฝ”๋“œ ํ˜ธํ™˜์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + fun validatePOCCompatibility(): Boolean { + // POC ์ฝ”๋“œ์˜ ํ„ฐ๋ฏธ๋„ ๊ฐœ์ˆ˜: 23๊ฐœ + val expectedTerminalCount = 23 + val actualTerminalCount = getTerminals().size + + // POC ์ฝ”๋“œ์˜ ๋…ผํ„ฐ๋ฏธ๋„ ๊ฐœ์ˆ˜: 9๊ฐœ + val expectedNonTerminalCount = 9 + val actualNonTerminalCount = getNonTerminals().size + + return actualTerminalCount == expectedTerminalCount && + actualNonTerminalCount == expectedNonTerminalCount + } + + /** + * ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getStatistics(): Map { + return mapOf( + "totalTokenTypes" to values().size, + "terminals" to getTerminals().size, + "nonTerminals" to getNonTerminals().size, + "operators" to getOperators().size, + "keywords" to getKeywords().size, + "literals" to getLiterals().size, + "pocCompatible" to validatePOCCompatibility() + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/exceptions/LexerException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/exceptions/LexerException.kt new file mode 100644 index 00000000..bdbc88ac --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/exceptions/LexerException.kt @@ -0,0 +1,1032 @@ +package hs.kr.entrydsm.domain.lexer.exceptions + +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * Lexer ๋„๋ฉ”์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ํ† ํฐํ™” ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ž, ์ž˜๋ชป๋œ ํ† ํฐ ์‹œํ€€์Šค, + * ์ˆซ์ž ํ˜•์‹ ์˜ค๋ฅ˜ ๋“ฑ์˜ ์–ดํœ˜ ๋ถ„์„ ๊ด€๋ จ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @property position ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ์ž…๋ ฅ ์œ„์น˜ (์„ ํƒ์‚ฌํ•ญ) + * @property character ์˜ค๋ฅ˜๋ฅผ ๋ฐœ์ƒ์‹œํ‚จ ๋ฌธ์ž (์„ ํƒ์‚ฌํ•ญ) + * @property token ์˜ค๋ฅ˜์™€ ๊ด€๋ จ๋œ ํ† ํฐ ์ •๋ณด (์„ ํƒ์‚ฌํ•ญ) + * @property reason ์‚ฌ์œ  (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +class LexerException( + errorCode: ErrorCode, + val position: Int? = null, + val character: Char? = null, + val token: String? = null, + val reason: String? = null, + message: String = buildLexerMessage(errorCode, position, character, token, reason), + cause: Throwable? = null +) : DomainException(errorCode, message, cause) { + + companion object { + /** + * Lexer ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param errorCode ์˜ค๋ฅ˜ ์ฝ”๋“œ + * @param position ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์œ„์น˜ + * @param character ์˜ค๋ฅ˜ ๋ฌธ์ž + * @param token ๊ด€๋ จ ํ† ํฐ + * @param reason ์‚ฌ์œ  + * @return ๊ตฌ์„ฑ๋œ ๋ฉ”์‹œ์ง€ + */ + private fun buildLexerMessage( + errorCode: ErrorCode, + position: Int?, + character: Char?, + token: String?, + reason: String? + ): String { + val baseMessage = errorCode.description + val details = mutableListOf() + + position?.let { details.add("์œ„์น˜: $it") } + character?.let { details.add("๋ฌธ์ž: '$it'") } + token?.let { details.add("ํ† ํฐ: $it") } + reason?.let { details.add("์‚ฌ์œ : $it") } + + return if (details.isNotEmpty()) { + "$baseMessage (${details.joinToString(", ")})" + } else { + baseMessage + } + } + + /** + * ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ž ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param character ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ž + * @param position ๋ฌธ์ž ์œ„์น˜ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unexpectedCharacter(character: Char, position: Int): LexerException { + return LexerException( + errorCode = ErrorCode.UNEXPECTED_CHARACTER, + character = character, + position = position + ) + } + + /** + * ๋‹ซํžˆ์ง€ ์•Š์€ ๋ณ€์ˆ˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๋‹ซํžˆ์ง€ ์•Š์€ ๋ณ€์ˆ˜ ํ† ํฐ + * @param position ์‹œ์ž‘ ์œ„์น˜ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unclosedVariable(token: String, position: Int): LexerException { + return LexerException( + errorCode = ErrorCode.UNCLOSED_VARIABLE, + token = token, + position = position + ) + } + + /** + * ์ž˜๋ชป๋œ ์ˆซ์ž ํ˜•์‹ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ์ž˜๋ชป๋œ ์ˆซ์ž ํ† ํฐ + * @param position ํ† ํฐ ์œ„์น˜ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidNumberFormat(token: String, position: Int): LexerException { + return LexerException( + errorCode = ErrorCode.INVALID_NUMBER_FORMAT, + token = token, + position = position + ) + } + + /** + * ์ž˜๋ชป๋œ ํ† ํฐ ์‹œํ€€์Šค ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ์ž˜๋ชป๋œ ํ† ํฐ + * @param position ํ† ํฐ ์œ„์น˜ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidTokenSequence(token: String, position: Int): LexerException { + return LexerException( + errorCode = ErrorCode.INVALID_TOKEN_SEQUENCE, + token = token, + position = position + ) + } + + /** + * ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ž ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (line, column ์ •๋ณด ํฌํ•จ). + * + * @param character ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ž + * @param position ๋ฌธ์ž ์œ„์น˜ + * @param line ์ค„ ๋ฒˆํ˜ธ + * @param column ์—ด ๋ฒˆํ˜ธ + * @param message ์ถ”๊ฐ€ ๋ฉ”์‹œ์ง€ (์„ ํƒ์‚ฌํ•ญ) + * @param cause ์›์ธ ์˜ˆ์™ธ (์„ ํƒ์‚ฌํ•ญ) + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unexpectedCharacter( + character: String, + position: Int, + line: Int, + column: Int, + message: String? = null, + cause: Throwable? = null + ): LexerException { + val finalMessage = message ?: "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ž '$character' (์œ„์น˜: $position, ์ค„: $line, ์—ด: $column)" + return LexerException( + errorCode = ErrorCode.UNEXPECTED_CHARACTER, + token = character, + position = position, + message = finalMessage, + cause = cause + ) + } + + /** + * ๋‹ซํžˆ์ง€ ์•Š์€ ๋ณ€์ˆ˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (line, column ์ •๋ณด ํฌํ•จ). + * + * @param variableName ๋ณ€์ˆ˜๋ช… + * @param position ์‹œ์ž‘ ์œ„์น˜ + * @param line ์ค„ ๋ฒˆํ˜ธ + * @param column ์—ด ๋ฒˆํ˜ธ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unclosedVariable( + variableName: String, + position: Int, + line: Int, + column: Int + ): LexerException { + val message = "๋‹ซํžˆ์ง€ ์•Š์€ ๋ณ€์ˆ˜ '$variableName' (์œ„์น˜: $position, ์ค„: $line, ์—ด: $column)" + return LexerException( + errorCode = ErrorCode.UNCLOSED_VARIABLE, + token = variableName, + position = position, + message = message + ) + } + + /** + * ์ž˜๋ชป๋œ ํ† ํฐ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ์ž˜๋ชป๋œ ํ† ํฐ + * @param position ํ† ํฐ ์œ„์น˜ + * @param line ์ค„ ๋ฒˆํ˜ธ + * @param column ์—ด ๋ฒˆํ˜ธ + * @param message ์ถ”๊ฐ€ ๋ฉ”์‹œ์ง€ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidToken( + token: String, + position: Int, + line: Int, + column: Int, + message: String + ): LexerException { + val finalMessage = "$message (ํ† ํฐ: '$token', ์œ„์น˜: $position, ์ค„: $line, ์—ด: $column)" + return LexerException( + errorCode = ErrorCode.INVALID_TOKEN_SEQUENCE, + token = token, + position = position, + message = finalMessage + ) + } + + /** + * ๋‹ซํžˆ์ง€ ์•Š์€ ๋ฌธ์ž์—ด ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param content ๋ฌธ์ž์—ด ๋‚ด์šฉ + * @param position ์‹œ์ž‘ ์œ„์น˜ + * @param line ์ค„ ๋ฒˆํ˜ธ + * @param column ์—ด ๋ฒˆํ˜ธ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unclosedString( + content: String, + position: Int, + line: Int, + column: Int + ): LexerException { + val message = "๋‹ซํžˆ์ง€ ์•Š์€ ๋ฌธ์ž์—ด (์œ„์น˜: $position, ์ค„: $line, ์—ด: $column)" + return LexerException( + errorCode = ErrorCode.INVALID_TOKEN_SEQUENCE, + token = "\"$content", + position = position, + message = message + ) + } + + /** + * ์ž˜๋ชป๋œ ์ˆซ์ž ํ˜•์‹ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (line, column ์ •๋ณด ํฌํ•จ). + * + * @param number ์ž˜๋ชป๋œ ์ˆซ์ž ๋ฌธ์ž์—ด + * @param position ํ† ํฐ ์œ„์น˜ + * @param line ์ค„ ๋ฒˆํ˜ธ + * @param column ์—ด ๋ฒˆํ˜ธ + * @param cause ์›์ธ ์˜ˆ์™ธ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidNumberFormat( + number: String, + position: Int, + line: Int, + column: Int, + cause: Throwable? = null + ): LexerException { + val message = "์ž˜๋ชป๋œ ์ˆซ์ž ํ˜•์‹ '$number' (์œ„์น˜: $position, ์ค„: $line, ์—ด: $column)" + return LexerException( + errorCode = ErrorCode.INVALID_NUMBER_FORMAT, + token = number, + position = position, + message = message, + cause = cause + ) + } + + /** + * EOF ํ† ํฐ์„ ์ œ์™ธํ•˜๊ณ  ํ† ํฐ ๊ฐ’์ด ๋น„์–ด์žˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun tokenValueEmptyExceptEof(type: String): LexerException = + LexerException( + errorCode = ErrorCode.TOKEN_VALUE_EMPTY_EXCEPT_EOF, + reason = "type=$type" + ) + + /** + * ๋ณ€์ˆ˜๋ช…์ด ๋น„์–ด์žˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์ž…๋ ฅ๋œ ๋ณ€์ˆ˜๋ช… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun variableNameEmpty(actual: String?): LexerException = + LexerException( + errorCode = ErrorCode.LEXER_VARIABLE_NAME_EMPTY, + token = actual, + reason = "actual=${actual ?: "null"}" + ) + + /** + * ์—ฐ์‚ฐ์ž ํƒ€์ž…์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun notOperatorType(type: String): LexerException = + LexerException( + errorCode = ErrorCode.NOT_OPERATOR_TYPE, + reason = "type=$type" + ) + + /** + * ์ˆซ์ž ํ† ํฐ์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun notNumberToken(type: String): LexerException = + LexerException( + errorCode = ErrorCode.NOT_NUMBER_TOKEN, + reason = "type=$type" + ) + + /** + * ๋ถˆ๋ฆฐ ํ† ํฐ์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun notBooleanToken(type: String): LexerException = + LexerException( + errorCode = ErrorCode.NOT_BOOLEAN_TOKEN, + reason = "type=$type" + ) + + /** + * ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ถˆ๋ฆฐ ํ† ํฐ ํƒ€์ž…์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unexpectedBooleanTokenType(type: String): LexerException = + LexerException( + errorCode = ErrorCode.UNEXPECTED_BOOLEAN_TOKEN_TYPE, + reason = "type=$type" + ) + + /** + * ์ž˜๋ชป๋œ ์ˆซ์ž ํ˜•์‹์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenStr ์ž˜๋ชป๋œ ์ˆซ์ž ํ† ํฐ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidNumberFormat(tokenStr: String): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_NUMBER_FORMAT, + token = tokenStr + ) + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ ์‹๋ณ„์ž ํ˜•์‹์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž˜๋ชป๋œ ์‹๋ณ„์ž ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidIdentifierFormat(value: String): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_IDENTIFIER_FORMAT, + token = value + ) + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ ์‹๋ณ„์ž์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž…๋ ฅ๋œ ์‹๋ณ„์ž ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidIdentifier(value: String): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_IDENTIFIER, + token = value + ) + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช…์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž…๋ ฅ๋œ ๋ณ€์ˆ˜๋ช… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidVariableName(value: String): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_VARIABLE_NAME, + token = value + ) + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedOperator(operator: String): LexerException = + LexerException( + errorCode = ErrorCode.LEXER_UNSUPPORTED_OPERATOR, + token = operator + ) + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ตฌ๋ถ„์ž์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param delimiter ๊ตฌ๋ถ„์ž ๋ฌธ์ž์—ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedDelimiter(delimiter: String): LexerException = + LexerException( + errorCode = ErrorCode.UNSUPPORTED_DELIMITER, + token = delimiter + ) + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ถˆ๋ฆฐ ๊ฐ’์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž…๋ ฅ๋œ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidBooleanValue(value: String): LexerException = + LexerException( + errorCode = ErrorCode.LEXER_INVALID_BOOLEAN_VALUE, + token = value + ) + + /** + * ์ธ์‹ํ•  ์ˆ˜ ์—†๋Š” ํ† ํฐ ๊ฐ’์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž…๋ ฅ๋œ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unrecognizedTokenValue(value: String): LexerException = + LexerException( + errorCode = ErrorCode.UNRECOGNIZED_TOKEN_VALUE, + token = value + ) + + /** + * NUMBER ํƒ€์ž… ํ† ํฐ ๊ฐ’์ด ์œ ํšจํ•œ ์ˆซ์ž๊ฐ€ ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž…๋ ฅ๋œ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun numberTokenInvalid(value: String): LexerException = + LexerException( + errorCode = ErrorCode.NUMBER_TOKEN_INVALID_NUMBER, + token = value + ) + + /** + * IDENTIFIER ํƒ€์ž… ํ† ํฐ ๊ฐ’์ด ์œ ํšจํ•œ ์‹๋ณ„์ž๊ฐ€ ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž…๋ ฅ๋œ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun identifierTokenInvalid(value: String): LexerException = + LexerException( + errorCode = ErrorCode.IDENTIFIER_TOKEN_INVALID, + token = value + ) + + /** + * VARIABLE ํƒ€์ž… ํ† ํฐ ๊ฐ’์ด ์œ ํšจํ•œ ๋ณ€์ˆ˜๋ช…์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž…๋ ฅ๋œ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun variableTokenInvalid(value: String): LexerException = + LexerException( + errorCode = ErrorCode.VARIABLE_TOKEN_INVALID, + token = value + ) + + /** + * ๋ถˆ๋ฆฐ ํƒ€์ž… ํ† ํฐ ๊ฐ’์ด 'true' ๋˜๋Š” 'false'๊ฐ€ ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž…๋ ฅ๋œ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun booleanTokenInvalid(value: String): LexerException = + LexerException( + errorCode = ErrorCode.BOOLEAN_TOKEN_INVALID, + token = value + ) + /** + * ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๋ฌธ์ž๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ๋ฐœ๊ฒฌ๋œ ๋ฌธ์ž + * @param codePoint ํ•ด๋‹น ๋ฌธ์ž์˜ ์ฝ”๋“œ ํฌ์ธํŠธ + */ + fun unallowedCharacter(char: Char, codePoint: Int): LexerException = + LexerException( + errorCode = ErrorCode.UNALLOWED_CHARACTER, + reason = "char='$char', codePoint=$codePoint" + ) + + /** + * ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ž๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param ch ๋ฐœ๊ฒฌ๋œ ๋ฌธ์ž + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unexpectedCharacter(ch: Char): LexerException = + LexerException(errorCode = ErrorCode.UNEXPECTED_CHARACTER, character = ch) + + /** + * ๋ณ€์ˆ˜๊ฐ€ ๋‹ซํžˆ์ง€ ์•Š์•˜์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenStr ๊ด€๋ จ ํ† ํฐ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun unclosedVariable(tokenStr: String? = null): LexerException = + LexerException(errorCode = ErrorCode.UNCLOSED_VARIABLE, token = tokenStr) + + /** + * ์ž˜๋ชป๋œ ํ† ํฐ ์‹œํ€€์Šค์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param reason ์˜ค๋ฅ˜ ์‚ฌ์œ  + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidTokenSequence(reason: String): LexerException = + LexerException(errorCode = ErrorCode.INVALID_TOKEN_SEQUENCE, reason = reason) + + // โ”€โ”€ ํ™•์žฅ(๊ฒ€์ฆ/๊ทœ์น™) ํŒฉํ† ๋ฆฌ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + + /** + * ๊ฒ€์ฆํ•  ํ† ํฐ ๋ชฉ๋ก์ด ๋น„์–ด์žˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun tokensEmpty(): LexerException = + LexerException(errorCode = ErrorCode.TOKENS_EMPTY) + + /** + * ํ† ํฐ ํƒ€์ž…์ด ๊ธฐ๋Œ€ํ•œ ํƒ€์ž…๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ํ•œ ํƒ€์ž… + * @param actual ์‹ค์ œ ํƒ€์ž… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun tokenTypeMismatch(expected: String, actual: String): LexerException = + LexerException(errorCode = ErrorCode.TOKEN_TYPE_MISMATCH, reason = "expected=$expected, actual=$actual") + + /** + * ์ˆซ์ž ๊ฐ’์ด ์œ ํ•œํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์œ ํ•œํ•˜์ง€ ์•Š์€ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun numberNotFinite(value: Double): LexerException = + LexerException(errorCode = ErrorCode.NUMBER_NOT_FINITE, reason = "value=$value") + + /** + * ์ˆซ์ž ๊ฐ’์ด ํ—ˆ์šฉ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์‹ค์ œ ๊ฐ’ + * @param min ์ตœ์†Œ ํ—ˆ์šฉ ๊ฐ’ + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun numberOutOfRange(value: Double, min: Double, max: Double): LexerException = + LexerException(errorCode = ErrorCode.NUMBER_OUT_OF_RANGE, reason = "value=$value, range=$min..$max") + + /** + * ์‹๋ณ„์ž ํ† ํฐ์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํ† ํฐ ํƒ€์ž… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun notIdentifierToken(actualType: String): LexerException = + LexerException(errorCode = ErrorCode.NOT_IDENTIFIER_TOKEN, reason = "actual=$actualType") + + /** + * ์‹๋ณ„์ž ๊ฐ’์ด ๋น„์–ด์žˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun identifierEmpty(): LexerException = + LexerException(errorCode = ErrorCode.IDENTIFIER_EMPTY) + + /** + * ์‹๋ณ„์ž ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๊ธธ์ด + * @param max ํ—ˆ์šฉ ์ตœ๋Œ€ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun identifierTooLong(actual: Int, max: Int): LexerException = + LexerException(errorCode = ErrorCode.IDENTIFIER_TOO_LONG, reason = "length=$actual, max=$max") + + /** + * ์‹๋ณ„์ž ํ˜•์‹์ด ์ž˜๋ชป๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž˜๋ชป๋œ ์‹๋ณ„์ž ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun identifierInvalidFormat(value: String): LexerException = + LexerException(errorCode = ErrorCode.IDENTIFIER_INVALID_FORMAT, token = value) + + /** + * ๋ณ€์ˆ˜ ํ† ํฐ์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํ† ํฐ ํƒ€์ž… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun notVariableToken(actualType: String): LexerException = + LexerException(errorCode = ErrorCode.NOT_VARIABLE_TOKEN, reason = "actual=$actualType") + + /** + * ๋ณ€์ˆ˜๋ช… ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๊ธธ์ด + * @param max ํ—ˆ์šฉ ์ตœ๋Œ€ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun variableNameTooLong(actual: Int, max: Int): LexerException = + LexerException(errorCode = ErrorCode.VARIABLE_NAME_TOO_LONG, reason = "length=$actual, max=$max") + + /** + * ๋ณ€์ˆ˜๋ช… ํ˜•์‹์ด ์ž˜๋ชป๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž˜๋ชป๋œ ๋ณ€์ˆ˜๋ช… ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun variableNameInvalidFormat(value: String): LexerException = + LexerException(errorCode = ErrorCode.VARIABLE_NAME_INVALID_FORMAT, token = value) + + /** + * ์—ฐ์‚ฐ์ž ๊ฐ’์ด ๋น„์–ด์žˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun operatorValueEmpty(): LexerException = + LexerException(errorCode = ErrorCode.OPERATOR_VALUE_EMPTY) + + /** + * ์œ ํšจํ•˜์ง€ ์•Š์€ ์—ฐ์‚ฐ์ž ์‹œํ€€์Šค์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param current ํ˜„์žฌ ์—ฐ์‚ฐ์ž ๊ฐ’ + * @param next ๋‹ค์Œ ์—ฐ์‚ฐ์ž ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidOperatorSequence(current: String, next: String): LexerException = + LexerException(errorCode = ErrorCode.INVALID_OPERATOR_SEQUENCE, reason = "seq='$current $next'") + + /** + * ํ‚ค์›Œ๋“œ ํ† ํฐ์ด ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualType ์‹ค์ œ ํ† ํฐ ํƒ€์ž… + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun notKeywordToken(actualType: String): LexerException = + LexerException(errorCode = ErrorCode.NOT_KEYWORD_TOKEN, reason = "actual=$actualType") + + /** + * ํ† ํฐ ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๊ธธ์ด + * @param max ํ—ˆ์šฉ ์ตœ๋Œ€ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun tokenTooLong(actual: Int, max: Int): LexerException = + LexerException(errorCode = ErrorCode.TOKEN_TOO_LONG, reason = "length=$actual, max=$max") + + /** + * EOF ํ† ํฐ์ด ์—ฌ๋Ÿฌ ๊ฐœ ์กด์žฌํ•  ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param count EOF ํ† ํฐ ๊ฐœ์ˆ˜ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun multipleEofTokens(count: Int): LexerException = + LexerException(errorCode = ErrorCode.MULTIPLE_EOF_TOKENS, reason = "count=$count") + + /** + * EOF ํ† ํฐ์ด ๋งˆ์ง€๋ง‰ ์œ„์น˜์— ์žˆ์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun eofNotAtEnd(): LexerException = + LexerException(errorCode = ErrorCode.EOF_NOT_AT_END) + + /** + * DOLLAR ํ† ํฐ ๊ฐ’์ด '$'๊ฐ€ ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ํ† ํฐ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun dollarTokenInvalidValue(actual: String): LexerException = + LexerException(errorCode = ErrorCode.DOLLAR_TOKEN_INVALID_VALUE, token = actual) + + /** + * NUMBER ํƒ€์ž…์ด์ง€๋งŒ ์ˆซ์ž๊ฐ€ ์•„๋‹ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ž˜๋ชป๋œ ์ˆซ์ž ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun numberTokenNotNumeric(value: String): LexerException = + LexerException( + errorCode = ErrorCode.NUMBER_TOKEN_NOT_NUMERIC, + token = value + ) + + /** + * ํ‚ค์›Œ๋“œ ๊ฐ’์ด ๊ธฐ๋Œ€์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ธฐ๋Œ€ํ•˜๋Š” ํ‚ค์›Œ๋“œ ๊ฐ’ + * @param actual ์‹ค์ œ ํ‚ค์›Œ๋“œ ๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun keywordValueMismatch(expected: String?, actual: String): LexerException = + LexerException( + errorCode = ErrorCode.KEYWORD_VALUE_MISMATCH, + reason = "expected=$expected, actual=$actual" + ) + + /** + * ์ž…๋ ฅ ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์ž…๋ ฅ ๊ธธ์ด + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun inputLengthExceeded(actual: Int, max: Int): LexerException = + LexerException( + errorCode = ErrorCode.INPUT_LENGTH_EXCEEDED, + reason = "actual=$actual, max=$max" + ) + + /** + * ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ๋ฌธ์ œ์˜ ๋ฌธ์ž + * @param codePoint ๋ฌธ์ž ์ฝ”๋“œ ํฌ์ธํŠธ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun disallowedCharacter(char: Char, codePoint: Int): LexerException = + LexerException( + errorCode = ErrorCode.DISALLOWED_CHARACTER, + reason = "char='$char', codePoint=$codePoint" + ) + + /** + * ๊ธˆ์ง€๋œ ์ œ์–ด ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param codePoint ์ œ์–ด ๋ฌธ์ž ์ฝ”๋“œ ํฌ์ธํŠธ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun forbiddenControlCharacter(codePoint: Int): LexerException = + LexerException( + errorCode = ErrorCode.FORBIDDEN_CONTROL_CHARACTER, + reason = "codePoint=$codePoint" + ) + + /** + * ๋ผ์ธ ์ˆ˜๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๋ผ์ธ ์ˆ˜ + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๋ผ์ธ ์ˆ˜ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun lineCountExceeded(actual: Int, max: Int): LexerException = + LexerException( + errorCode = ErrorCode.LINE_COUNT_EXCEEDED, + reason = "actual=$actual, max=$max" + ) + + /** + * ๋ผ์ธ ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param lineIndex ๋ผ์ธ ์ธ๋ฑ์Šค + * @param actual ์‹ค์ œ ๊ธธ์ด + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun lineLengthExceeded(lineIndex: Int, actual: Int, max: Int): LexerException = + LexerException( + errorCode = ErrorCode.LINE_LENGTH_EXCEEDED, + reason = "line=${lineIndex + 1}, actual=$actual, max=$max" + ) + + /** + * BOM ๋ฌธ์ž๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun bomCharacterDetected(): LexerException = + LexerException(errorCode = ErrorCode.BOM_CHARACTER_DETECTED) + + /** + * ๋„ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun nullCharacterDetected(): LexerException = + LexerException(errorCode = ErrorCode.NULL_CHARACTER_DETECTED) + + /** + * ์ค‘์ฒฉ ๊นŠ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๊นŠ์ด + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ๊นŠ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun maxNestingDepthExceeded(actual: Int, max: Int): LexerException = + LexerException( + errorCode = ErrorCode.MAX_NESTING_DEPTH_EXCEEDED, + reason = "actual=$actual, max=$max" + ) + + /** + * ๊ณผ๋„ํ•œ ์—ฐ์† ๊ณต๋ฐฑ์ด ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun excessiveWhitespaceDetected(): LexerException = + LexerException(errorCode = ErrorCode.EXCESSIVE_WHITESPACE_DETECTED) + + /** + * ์˜์‹ฌ์Šค๋Ÿฌ์šด ๋ฐ˜๋ณต ํŒจํ„ด์ด ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun suspiciousRepeatPattern(): LexerException = + LexerException(errorCode = ErrorCode.SUSPICIOUS_REPEAT_PATTERN) + + /** + * ์œ„์น˜ ์ธ๋ฑ์Šค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์ธ๋ฑ์Šค + * @param max ์ตœ๋Œ€ ํ—ˆ์šฉ ์ธ๋ฑ์Šค + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidPositionIndex(actual: Int, max: Int): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_POSITION_INDEX, + reason = "actual=$actual, max=$max" + ) + + /** + * ๋ผ์ธ ๋ฒˆํ˜ธ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๋ผ์ธ ๋ฒˆํ˜ธ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidPositionLine(actual: Int): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_POSITION_LINE, + reason = "actual=$actual" + ) + + /** + * ์—ด ๋ฒˆํ˜ธ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์—ด ๋ฒˆํ˜ธ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidPositionColumn(actual: Int): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_POSITION_COLUMN, + reason = "actual=$actual" + ) + + /** + * ์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด ๊ฐ’์ด ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์„ค์ •๊ฐ’ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidMaxTokenLength(actual: Int): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_MAX_TOKEN_LENGTH, + reason = "actual=$actual" + ) + + /** + * ์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด + * @param max ์‹œ์Šคํ…œ ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun maxTokenLengthExceeded(actual: Int, max: Int): LexerException = + LexerException( + errorCode = ErrorCode.MAX_TOKEN_LENGTH_EXCEEDED, + reason = "actual=$actual, max=$max" + ) + + /** + * ์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด๊ฐ€ 1 ๋ฏธ๋งŒ์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun maxTokenLengthInvalid(actual: Int): LexerException = + LexerException( + errorCode = ErrorCode.MAX_TOKEN_LENGTH_INVALID, + reason = "actual=$actual" + ) + + /** + * ์‹œ์ž‘ ์‹œ๊ฐ„์ด ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์‹œ์ž‘ ์‹œ๊ฐ„ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun startTimeInvalid(actual: Long): LexerException = + LexerException( + errorCode = ErrorCode.START_TIME_INVALID, + reason = "actual=$actual" + ) + + /** + * ์ด๋™ ๊ฑฐ๋ฆฌ๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์ด๋™ ๊ฑฐ๋ฆฌ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun stepsNegative(actual: Int): LexerException = + LexerException( + errorCode = ErrorCode.STEPS_NEGATIVE, + reason = "actual=$actual" + ) + + /** + * ์‹คํŒจํ•œ LexingResult์— error ์ •๋ณด๊ฐ€ ์—†์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param isSuccess ์„ฑ๊ณต ์—ฌ๋ถ€ + * @param error ์‹ค์ œ ์—๋Ÿฌ ๊ฐ์ฒด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidLexingResultErrorState(isSuccess: Boolean, error: Throwable?): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_LEXING_RESULT_ERROR_STATE, + reason = "isSuccess=$isSuccess, error=${error?.javaClass?.simpleName ?: "null"}" + ) + + /** + * ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„์ด 0 ๋ฏธ๋งŒ์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun negativeAnalysisDuration(actual: Long): LexerException = + LexerException( + errorCode = ErrorCode.NEGATIVE_ANALYSIS_DURATION, + reason = "actual=$actual" + ) + + /** + * ์ž…๋ ฅ ํ…์ŠคํŠธ ๊ธธ์ด๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์ž…๋ ฅ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun negativeInputLength(actual: Int): LexerException = + LexerException( + errorCode = ErrorCode.NEGATIVE_INPUT_LENGTH, + reason = "actual=$actual" + ) + + /** + * ํ† ํฐ ๊ฐœ์ˆ˜๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ํ† ํฐ ๊ฐœ์ˆ˜ + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun negativeTokenCount(actual: Int): LexerException = + LexerException( + errorCode = ErrorCode.NEGATIVE_TOKEN_COUNT, + reason = "actual=$actual" + ) + + /** + * ์‹œ์ž‘ ์œ„์น˜๊ฐ€ ๋ ์œ„์น˜๋ณด๋‹ค ๋Šฆ์„ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startIndex ์‹œ์ž‘ ์ธ๋ฑ์Šค + * @param endIndex ๋ ์ธ๋ฑ์Šค + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun invalidPositionOrder(startIndex: Int, endIndex: Int): LexerException = + LexerException( + errorCode = ErrorCode.INVALID_POSITION_ORDER, + reason = "startIndex=$startIndex, endIndex=$endIndex" + ) + + /** + * ํ† ํฐ ๊ธธ์ด๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ํ† ํฐ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun negativeTokenLength(actual: Int): LexerException = + LexerException( + errorCode = ErrorCode.NEGATIVE_TOKEN_LENGTH, + reason = "actual=$actual" + ) + + /** + * ์ถ”๊ฐ€ ๊ธธ์ด๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actual ์‹ค์ œ ์ถ”๊ฐ€ ๊ธธ์ด + * @return LexerException ์ธ์Šคํ„ด์Šค + */ + fun negativeAdditionalLength(actual: Int): LexerException = + LexerException( + errorCode = ErrorCode.NEGATIVE_ADDITIONAL_LENGTH, + reason = "actual=$actual" + ) + } + + /** + * Lexer ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ„์น˜, ๋ฌธ์ž, ํ† ํฐ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋งต + */ + fun getLexerInfo(): Map = mapOf( + "position" to position, + "character" to character, + "token" to token + ).filterValues { it != null } + + /** + * ์ „์ฒด ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ์ •๋ณด์™€ Lexer ์ •๋ณด๊ฐ€ ๊ฒฐํ•ฉ๋œ ๋งต + */ + fun getLexerErrorInfo(): Map { + val baseInfo = super.toErrorInfo().toMutableMap() + val lexerInfo = getLexerInfo() + + lexerInfo.forEach { (key, value) -> + baseInfo[key] = value.toString() + } + + return baseInfo + } + + override fun toString(): String { + val lexerDetails = getLexerInfo() + return if (lexerDetails.isNotEmpty()) { + "${super.toString()}, lexer=${lexerDetails}" + } else { + super.toString() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/factories/TokenFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/factories/TokenFactory.kt new file mode 100644 index 00000000..bab004e2 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/factories/TokenFactory.kt @@ -0,0 +1,322 @@ +package hs.kr.entrydsm.domain.lexer.factories + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.lexer.exceptions.LexerException +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.global.values.Position + +/** + * ํ† ํฐ ์ƒ์„ฑ์„ ๋‹ด๋‹นํ•˜๋Š” ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Factory ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๋ณต์žกํ•œ ํ† ํฐ ์ƒ์„ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ , + * ์ผ๊ด€๋œ ํ† ํฐ ๊ฐ์ฒด ์ƒ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ํƒ€์ž…์˜ ํ† ํฐ์„ ์ƒ์„ฑํ•˜๋ฉฐ, + * ์œ„์น˜ ์ •๋ณด์™€ ๊ฒ€์ฆ ๋กœ์ง์„ ํฌํ•จํ•œ ์™„์ „ํ•œ ํ† ํฐ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Factory(context = "lexer", complexity = Complexity.NORMAL, cache = true) +class TokenFactory { + + companion object { + private val KEYWORD_MAP = mapOf( + "if" to TokenType.IF, + "true" to TokenType.TRUE, + "false" to TokenType.FALSE, + "and" to TokenType.AND, + "or" to TokenType.OR, + "not" to TokenType.NOT, + "mod" to TokenType.MODULO + ) + + private val OPERATOR_MAP = mapOf( + "+" to TokenType.PLUS, + "-" to TokenType.MINUS, + "*" to TokenType.MULTIPLY, + "/" to TokenType.DIVIDE, + "^" to TokenType.POWER, + "%" to TokenType.MODULO, + "==" to TokenType.EQUAL, + "!=" to TokenType.NOT_EQUAL, + "<" to TokenType.LESS, + "<=" to TokenType.LESS_EQUAL, + ">" to TokenType.GREATER, + ">=" to TokenType.GREATER_EQUAL, + "&&" to TokenType.AND, + "||" to TokenType.OR, + "!" to TokenType.NOT + ) + + private val DELIMITER_MAP = mapOf( + "(" to TokenType.LEFT_PAREN, + ")" to TokenType.RIGHT_PAREN, + "," to TokenType.COMMA + ) + } + + /** + * ๋ฌธ์ž์—ด ๊ฐ’๊ณผ ์œ„์น˜ ์ •๋ณด๋กœ ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ํ† ํฐ ๊ฐ’ + * @param position ํ† ํฐ ์œ„์น˜ + * @return ์ƒ์„ฑ๋œ Token + */ + fun createToken(value: String, position: Position): Token { + val type = determineTokenType(value) + return Token(type, value, position) + } + + /** + * ํ† ํฐ ํƒ€์ž…๊ณผ ๊ฐ’, ์œ„์น˜๋กœ ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @param value ํ† ํฐ ๊ฐ’ + * @param position ํ† ํฐ ์œ„์น˜ + * @return ์ƒ์„ฑ๋œ Token + */ + fun createToken(type: TokenType, value: String, position: Position): Token { + validateTokenData(type, value) + return Token(type, value, position) + } + + /** + * ์ˆซ์ž ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์ˆซ์ž ๋ฌธ์ž์—ด + * @param startPosition ์‹œ์ž‘ ์œ„์น˜ + * @return ์ˆซ์ž Token + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ์ˆซ์ž ํ˜•์‹์ธ ๊ฒฝ์šฐ + */ + fun createNumberToken(value: String, startPosition: Position): Token { + if (!isValidNumber(value)) { + throw LexerException.invalidNumberFormat(value) + } + + val position = startPosition + return Token(TokenType.NUMBER, value, position) + } + + /** + * ์‹๋ณ„์ž ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์‹๋ณ„์ž ๋ฌธ์ž์—ด + * @param startPosition ์‹œ์ž‘ ์œ„์น˜ + * @return ์‹๋ณ„์ž Token (ํ‚ค์›Œ๋“œ์ธ ๊ฒฝ์šฐ ํ•ด๋‹น ํ‚ค์›Œ๋“œ ํ† ํฐ) + */ + fun createIdentifierToken(value: String, startPosition: Position): Token { + if (!isValidIdentifier(value)) { + throw LexerException.invalidIdentifier(value) + } + + val position = startPosition + val type = KEYWORD_MAP[value.lowercase()] ?: TokenType.IDENTIFIER + + return Token(type, value, position) + } + + /** + * ๋ณ€์ˆ˜ ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param variableName ๋ณ€์ˆ˜๋ช… (์ค‘๊ด„ํ˜ธ ์ œ์™ธ) + * @param startPosition ์‹œ์ž‘ ์œ„์น˜ (์ค‘๊ด„ํ˜ธ ํฌํ•จ) + * @return ๋ณ€์ˆ˜ Token + */ + fun createVariableToken(variableName: String, startPosition: Position): Token { + if (variableName.isEmpty()) { + throw LexerException.variableNameEmpty(variableName) + } + + if (!isValidIdentifier(variableName)) { + throw LexerException.invalidVariableName(variableName) + } + + val position = startPosition // {๋ณ€์ˆ˜๋ช…} ํฌํ•จ + return Token(TokenType.VARIABLE, variableName, position) + } + + /** + * ์—ฐ์‚ฐ์ž ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @param startPosition ์‹œ์ž‘ ์œ„์น˜ + * @return ์—ฐ์‚ฐ์ž Token + * @throws IllegalArgumentException ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž์ธ ๊ฒฝ์šฐ + */ + fun createOperatorToken(operator: String, startPosition: Position): Token { + val type = OPERATOR_MAP[operator] + ?: throw LexerException.unsupportedOperator(operator) + + val position = startPosition + return Token(type, operator, position) + } + + /** + * ๊ตฌ๋ถ„์ž ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param delimiter ๊ตฌ๋ถ„์ž ๋ฌธ์ž์—ด + * @param startPosition ์‹œ์ž‘ ์œ„์น˜ + * @return ๊ตฌ๋ถ„์ž Token + * @throws IllegalArgumentException ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ตฌ๋ถ„์ž์ธ ๊ฒฝ์šฐ + */ + fun createDelimiterToken(delimiter: String, startPosition: Position): Token { + val type = DELIMITER_MAP[delimiter] + ?: throw LexerException.unsupportedDelimiter(delimiter) + + val position = startPosition + return Token(type, delimiter, position) + } + + /** + * EOF(End of File) ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param position EOF ์œ„์น˜ + * @return EOF Token + */ + fun createEOFToken(position: Position): Token { + val tokenPosition = position + return Token(TokenType.DOLLAR, "$", tokenPosition) + } + + /** + * ๋ถˆ๋ฆฐ ๋ฆฌํ„ฐ๋Ÿด ํ† ํฐ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value "true" ๋˜๋Š” "false" + * @param startPosition ์‹œ์ž‘ ์œ„์น˜ + * @return ๋ถˆ๋ฆฐ Token + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ถˆ๋ฆฐ ๊ฐ’์ธ ๊ฒฝ์šฐ + */ + fun createBooleanToken(value: String, startPosition: Position): Token { + val type = when (value.lowercase()) { + "true" -> TokenType.TRUE + "false" -> TokenType.FALSE + else -> throw LexerException.invalidBooleanValue(value) + } + + val position = startPosition + return Token(type, value, position) + } + + /** + * ๋ฌธ์ž์—ด๋กœ๋ถ€ํ„ฐ ํ† ํฐ ํƒ€์ž…์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ํ† ํฐ ๊ฐ’ + * @return ๊ฒฐ์ •๋œ TokenType + */ + private fun determineTokenType(value: String): TokenType = when { + value.isEmpty() -> throw LexerException.tokenValueEmptyExceptEof(value) + isValidNumber(value) -> TokenType.NUMBER + KEYWORD_MAP.containsKey(value.lowercase()) -> KEYWORD_MAP[value.lowercase()]!! + OPERATOR_MAP.containsKey(value) -> OPERATOR_MAP[value]!! + DELIMITER_MAP.containsKey(value) -> DELIMITER_MAP[value]!! + value == "$" -> TokenType.DOLLAR + isValidIdentifier(value) -> TokenType.IDENTIFIER + else -> throw LexerException.unrecognizedTokenValue(value) + } + + /** + * ์œ ํšจํ•œ ์ˆซ์ž ํ˜•์‹์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๊ฒ€์ฆํ•  ๋ฌธ์ž์—ด + * @return ์œ ํšจํ•œ ์ˆซ์ž์ด๋ฉด true + */ + private fun isValidNumber(value: String): Boolean { + return try { + value.toDouble() + value.matches(Regex("""^-?\d+(\.\d+)?$""")) + } catch (e: NumberFormatException) { + false + } + } + + /** + * ์œ ํšจํ•œ ์‹๋ณ„์ž ํ˜•์‹์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ๊ฒ€์ฆํ•  ๋ฌธ์ž์—ด + * @return ์œ ํšจํ•œ ์‹๋ณ„์ž์ด๋ฉด true + */ + private fun isValidIdentifier(value: String): Boolean { + return value.isNotEmpty() && + value.matches(Regex("""^[a-zA-Z_][a-zA-Z0-9_]*$""")) + } + + /** + * ํ† ํฐ ๋ฐ์ดํ„ฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ† ํฐ ํƒ€์ž… + * @param value ํ† ํฐ ๊ฐ’ + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ๋ฐ์ดํ„ฐ์ธ ๊ฒฝ์šฐ + */ + private fun validateTokenData(type: TokenType, value: String) { + if (value.isEmpty() && type != TokenType.DOLLAR) { + throw LexerException.tokenValueEmptyExceptEof(type.name) + } + + when (type) { + TokenType.NUMBER -> { + if (!isValidNumber(value)) { + throw LexerException.numberTokenInvalid(value) + } + } + TokenType.IDENTIFIER -> { + if (!isValidIdentifier(value)) { + throw LexerException.identifierTokenInvalid(value) + } + } + TokenType.VARIABLE -> { + if (!isValidIdentifier(value)) { + throw LexerException.variableTokenInvalid(value) + } + } + TokenType.TRUE, TokenType.FALSE -> { + val v = value.lowercase() + if (v != "true" && v != "false") { + throw LexerException.booleanTokenInvalid(value) + } + } + else -> { /* ๋‹ค๋ฅธ ํƒ€์ž…๋“ค์€ ์ถ”๊ฐ€ ๊ฒ€์ฆ ์—†์Œ */ } + } + } + + /** + * ํŒฉํ† ๋ฆฌ์—์„œ ์ง€์›ํ•˜๋Š” ํ† ํฐ ํƒ€์ž… ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›๋˜๋Š” TokenType ์ง‘ํ•ฉ + */ + fun getSupportedTokenTypes(): Set = setOf( + TokenType.NUMBER, + TokenType.IDENTIFIER, + TokenType.VARIABLE, + TokenType.DOLLAR, + *KEYWORD_MAP.values.toTypedArray(), + *OPERATOR_MAP.values.toTypedArray(), + *DELIMITER_MAP.values.toTypedArray() + ) + + /** + * ํŠน์ • ๋ฌธ์ž์—ด์ด ํ‚ค์›Œ๋“œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ํ‚ค์›Œ๋“œ์ด๋ฉด true + */ + fun isKeyword(value: String): Boolean = KEYWORD_MAP.containsKey(value.lowercase()) + + /** + * ํŠน์ • ๋ฌธ์ž์—ด์ด ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ์—ฐ์‚ฐ์ž์ด๋ฉด true + */ + fun isOperator(value: String): Boolean = OPERATOR_MAP.containsKey(value) + + /** + * ํŠน์ • ๋ฌธ์ž์—ด์ด ๊ตฌ๋ถ„์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ๊ตฌ๋ถ„์ž์ด๋ฉด true + */ + fun isDelimiter(value: String): Boolean = DELIMITER_MAP.containsKey(value) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/policies/CharacterRecognitionPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/policies/CharacterRecognitionPolicy.kt new file mode 100644 index 00000000..9f533565 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/policies/CharacterRecognitionPolicy.kt @@ -0,0 +1,344 @@ +package hs.kr.entrydsm.domain.lexer.policies + +import hs.kr.entrydsm.domain.lexer.exceptions.LexerException +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ๋ฌธ์ž ์ธ์‹ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Policy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์–ดํœ˜ ๋ถ„์„ ๊ณผ์ •์—์„œ ๋ฌธ์ž๋ฅผ ์–ด๋–ป๊ฒŒ ๋ถ„๋ฅ˜ํ•˜๊ณ  + * ์ฒ˜๋ฆฌํ• ์ง€์— ๋Œ€ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๋ฌธ์ž ํƒ€์ž… ํŒ๋ณ„, ํŠน์ˆ˜ ๋ฌธ์ž ์ฒ˜๋ฆฌ, + * ์œ ๋‹ˆ์ฝ”๋“œ ์ง€์› ๋“ฑ์˜ ์ •์ฑ…์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Policy( + name = "CharacterRecognition", + description = "์–ดํœ˜ ๋ถ„์„ ๊ณผ์ •์—์„œ ๋ฌธ์ž ๋ถ„๋ฅ˜ ๋ฐ ์ฒ˜๋ฆฌ์— ๋Œ€ํ•œ ์ •์ฑ…", + domain = "lexer", + scope = Scope.DOMAIN +) +class CharacterRecognitionPolicy { + + companion object { + private val WHITESPACE_CHARS = setOf(' ', '\t', '\n', '\r', '\u000c') + private val DIGIT_CHARS = '0'..'9' + private val LETTER_CHARS = ('a'..'z') + ('A'..'Z') + private val OPERATOR_START_CHARS = setOf('+', '-', '*', '/', '^', '%', '=', '!', '<', '>', '&', '|') + private val DELIMITER_CHARS = setOf('(', ')', ',', '{', '}', '[', ']') + private val IDENTIFIER_START_CHARS = LETTER_CHARS.toSet() + '_' + private val IDENTIFIER_BODY_CHARS = IDENTIFIER_START_CHARS + DIGIT_CHARS.toSet() + private val SPECIAL_CHARS = setOf('.', ';', ':', '"', '\'', '\\', '@', '#', '$') + private val COMMENT_START_CHARS = setOf('/', '#') + + // ์œ ๋‹ˆ์ฝ”๋“œ ๋ฒ”์œ„ (๊ธฐ๋ณธ์ ์œผ๋กœ ASCII๋งŒ ํ—ˆ์šฉ) + private const val MAX_ASCII_VALUE = 127 + } + + private var allowUnicode: Boolean = false + private var allowExtendedASCII: Boolean = false + private var caseSensitive: Boolean = true + private var allowUnderscoreInNumbers: Boolean = false + + /** + * ์ •์ฑ… ์„ค์ •์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param allowUnicode ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @param allowExtendedASCII ํ™•์žฅ ASCII ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @param caseSensitive ๋Œ€์†Œ๋ฌธ์ž ๊ตฌ๋ถ„ ์—ฌ๋ถ€ + * @param allowUnderscoreInNumbers ์ˆซ์ž์—์„œ ์–ธ๋”์Šค์ฝ”์–ด ๊ตฌ๋ถ„์ž ํ—ˆ์šฉ ์—ฌ๋ถ€ + */ + fun configure( + allowUnicode: Boolean = false, + allowExtendedASCII: Boolean = false, + caseSensitive: Boolean = true, + allowUnderscoreInNumbers: Boolean = false + ) { + this.allowUnicode = allowUnicode + this.allowExtendedASCII = allowExtendedASCII + this.caseSensitive = caseSensitive + this.allowUnderscoreInNumbers = allowUnderscoreInNumbers + } + + /** + * ๋ฌธ์ž๊ฐ€ ๊ณต๋ฐฑ ๋ฌธ์ž์ธ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ๊ณต๋ฐฑ ๋ฌธ์ž์ด๋ฉด true + */ + fun isWhitespace(char: Char): Boolean { + return char in WHITESPACE_CHARS || + (allowUnicode && char.isWhitespace()) + } + + /** + * ๋ฌธ์ž๊ฐ€ ์ˆซ์ž์ธ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ์ˆซ์ž์ด๋ฉด true + */ + fun isDigit(char: Char): Boolean { + return char in DIGIT_CHARS || + (allowUnicode && char.isDigit()) + } + + /** + * ๋ฌธ์ž๊ฐ€ ๋ฌธ์ž(์•ŒํŒŒ๋ฒณ)์ธ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ๋ฌธ์ž์ด๋ฉด true + */ + fun isLetter(char: Char): Boolean { + return char in LETTER_CHARS || + (allowUnicode && char.isLetter()) + } + + /** + * ๋ฌธ์ž๊ฐ€ ์‹๋ณ„์ž ์‹œ์ž‘์— ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋Š”์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ์‹๋ณ„์ž ์‹œ์ž‘์— ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun isIdentifierStart(char: Char): Boolean { + return char in IDENTIFIER_START_CHARS || + (allowUnicode && (char.isLetter() || char == '_')) + } + + /** + * ๋ฌธ์ž๊ฐ€ ์‹๋ณ„์ž ๋ณธ๋ฌธ์— ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋Š”์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ์‹๋ณ„์ž ๋ณธ๋ฌธ์— ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun isIdentifierBody(char: Char): Boolean { + return char in IDENTIFIER_BODY_CHARS || + (allowUnicode && (char.isLetterOrDigit() || char == '_')) + } + + /** + * ๋ฌธ์ž๊ฐ€ ์—ฐ์‚ฐ์ž ์‹œ์ž‘์— ์‚ฌ์šฉ๋  ์ˆ˜ ์žˆ๋Š”์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ์—ฐ์‚ฐ์ž ์‹œ์ž‘์— ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun isOperatorStart(char: Char): Boolean { + return char in OPERATOR_START_CHARS + } + + /** + * ๋ฌธ์ž๊ฐ€ ๊ตฌ๋ถ„์ž์ธ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ๊ตฌ๋ถ„์ž์ด๋ฉด true + */ + fun isDelimiter(char: Char): Boolean { + return char in DELIMITER_CHARS + } + + /** + * ๋ฌธ์ž๊ฐ€ ํŠน์ˆ˜ ๋ฌธ์ž์ธ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ํŠน์ˆ˜ ๋ฌธ์ž์ด๋ฉด true + */ + fun isSpecialChar(char: Char): Boolean { + return char in SPECIAL_CHARS + } + + /** + * ๋ฌธ์ž๊ฐ€ ์ฃผ์„ ์‹œ์ž‘ ๋ฌธ์ž์ธ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ์ฃผ์„ ์‹œ์ž‘ ๋ฌธ์ž์ด๋ฉด true + */ + fun isCommentStart(char: Char): Boolean { + return char in COMMENT_START_CHARS + } + + /** + * ๋ฌธ์ž๊ฐ€ ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด์—์„œ ์œ ํšจํ•œ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @param isFirstChar ์ฒซ ๋ฒˆ์งธ ๋ฌธ์ž์ธ์ง€ ์—ฌ๋ถ€ + * @return ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด์—์„œ ์œ ํšจํ•˜๋ฉด true + */ + fun isValidInNumber(char: Char, isFirstChar: Boolean = false): Boolean { + return when { + isDigit(char) -> true + char == '.' -> !isFirstChar // ์†Œ์ˆ˜์ ์€ ์ฒซ ๋ฒˆ์งธ๊ฐ€ ์•„๋‹ ๋•Œ๋งŒ + char == '-' -> isFirstChar // ์Œ์ˆ˜ ๊ธฐํ˜ธ๋Š” ์ฒซ ๋ฒˆ์งธ๋งŒ + char == '_' -> allowUnderscoreInNumbers && !isFirstChar // ๊ตฌ๋ถ„์ž + char in setOf('e', 'E') -> !isFirstChar // ๊ณผํ•™์  ํ‘œ๊ธฐ๋ฒ• + else -> false + } + } + + /** + * ๋ฌธ์ž๊ฐ€ ๋ณ€์ˆ˜๋ช… ๊ตฌ๋ถ„์ž(์ค‘๊ด„ํ˜ธ)์ธ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ๋ณ€์ˆ˜๋ช… ๊ตฌ๋ถ„์ž์ด๋ฉด true + */ + fun isVariableDelimiter(char: Char): Boolean { + return char == '{' || char == '}' + } + + /** + * ๋ฌธ์ž๊ฐ€ ๋ฌธ์ž์—ด ๊ตฌ๋ถ„์ž์ธ์ง€ ํŒ๋ณ„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํŒ๋ณ„ํ•  ๋ฌธ์ž + * @return ๋ฌธ์ž์—ด ๊ตฌ๋ถ„์ž์ด๋ฉด true + */ + fun isStringDelimiter(char: Char): Boolean { + return char == '"' || char == '\'' + } + + /** + * ๋ฌธ์ž๊ฐ€ ํ—ˆ์šฉ๋œ ๋ฌธ์ž์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ๊ฒ€์ฆํ•  ๋ฌธ์ž + * @return ํ—ˆ์šฉ๋œ ๋ฌธ์ž์ด๋ฉด true + * @throws IllegalArgumentException ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๋ฌธ์ž์ธ ๊ฒฝ์šฐ + */ + fun validateChar(char: Char): Boolean { + val codePoint = char.code + + when { + codePoint <= MAX_ASCII_VALUE -> return true // ASCII ๋ฒ”์œ„ + allowExtendedASCII && codePoint <= 255 -> return true // ํ™•์žฅ ASCII + allowUnicode -> return true // ์œ ๋‹ˆ์ฝ”๋“œ ์ „์ฒด + else -> throw LexerException.unallowedCharacter(char, codePoint) + } + } + + /** + * ๋ฌธ์ž๋ฅผ ์ •๊ทœํ™”ํ•ฉ๋‹ˆ๋‹ค (๋Œ€์†Œ๋ฌธ์ž ์ฒ˜๋ฆฌ ๋“ฑ). + * + * @param char ์ •๊ทœํ™”ํ•  ๋ฌธ์ž + * @return ์ •๊ทœํ™”๋œ ๋ฌธ์ž + */ + fun normalizeChar(char: Char): Char { + return if (!caseSensitive && char.isLetter()) { + char.lowercaseChar() + } else { + char + } + } + + /** + * ๋ฌธ์ž์—ด์„ ์ •๊ทœํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ์ •๊ทœํ™”ํ•  ๋ฌธ์ž์—ด + * @return ์ •๊ทœํ™”๋œ ๋ฌธ์ž์—ด + */ + fun normalizeText(text: String): String { + return if (!caseSensitive) { + text.lowercase() + } else { + text + } + } + + /** + * ๋‘ ๋ฌธ์ž๊ฐ€ ์ •์ฑ…์— ๋”ฐ๋ผ ๊ฐ™์€์ง€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + * + * @param char1 ์ฒซ ๋ฒˆ์งธ ๋ฌธ์ž + * @param char2 ๋‘ ๋ฒˆ์งธ ๋ฌธ์ž + * @return ์ •์ฑ…์— ๋”ฐ๋ผ ๊ฐ™์œผ๋ฉด true + */ + fun areCharsEqual(char1: Char, char2: Char): Boolean { + return normalizeChar(char1) == normalizeChar(char2) + } + + /** + * ๋‘ ๋ฌธ์ž์—ด์ด ์ •์ฑ…์— ๋”ฐ๋ผ ๊ฐ™์€์ง€ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + * + * @param text1 ์ฒซ ๋ฒˆ์งธ ๋ฌธ์ž์—ด + * @param text2 ๋‘ ๋ฒˆ์งธ ๋ฌธ์ž์—ด + * @return ์ •์ฑ…์— ๋”ฐ๋ผ ๊ฐ™์œผ๋ฉด true + */ + fun areTextsEqual(text1: String, text2: String): Boolean { + return normalizeText(text1) == normalizeText(text2) + } + + /** + * ๋ฌธ์ž์˜ ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ๋ถ„๋ฅ˜ํ•  ๋ฌธ์ž + * @return ๋ฌธ์ž ์นดํ…Œ๊ณ ๋ฆฌ + */ + fun getCharCategory(char: Char): CharCategory { + return when { + isWhitespace(char) -> CharCategory.WHITESPACE + isDigit(char) -> CharCategory.DIGIT + isLetter(char) -> CharCategory.LETTER + isOperatorStart(char) -> CharCategory.OPERATOR + isDelimiter(char) -> CharCategory.DELIMITER + isSpecialChar(char) -> CharCategory.SPECIAL + isVariableDelimiter(char) -> CharCategory.VARIABLE_DELIMITER + isStringDelimiter(char) -> CharCategory.STRING_DELIMITER + isCommentStart(char) -> CharCategory.COMMENT_START + else -> CharCategory.UNKNOWN + } + } + + /** + * ๋ฌธ์ž๊ฐ€ ํŠน์ • ์นดํ…Œ๊ณ ๋ฆฌ์— ์†ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํ™•์ธํ•  ๋ฌธ์ž + * @param category ํ™•์ธํ•  ์นดํ…Œ๊ณ ๋ฆฌ + * @return ํ•ด๋‹น ์นดํ…Œ๊ณ ๋ฆฌ์— ์†ํ•˜๋ฉด true + */ + fun isCharInCategory(char: Char, category: CharCategory): Boolean { + return getCharCategory(char) == category + } + + /** + * ์ •์ฑ…์˜ ํ˜„์žฌ ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "allowUnicode" to allowUnicode, + "allowExtendedASCII" to allowExtendedASCII, + "caseSensitive" to caseSensitive, + "allowUnderscoreInNumbers" to allowUnderscoreInNumbers, + "supportedCharCategories" to CharCategory.values().map { it.name } + ) + + /** + * ์ง€์›๋˜๋Š” ๋ฌธ์ž ์ง‘ํ•ฉ์˜ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฌธ์ž ์ง‘ํ•ฉ ํ†ต๊ณ„ + */ + fun getCharSetStatistics(): Map = mapOf( + "whitespaceChars" to WHITESPACE_CHARS.size, + "digitChars" to DIGIT_CHARS.count(), + "letterChars" to LETTER_CHARS.count(), + "operatorStartChars" to OPERATOR_START_CHARS.size, + "delimiterChars" to DELIMITER_CHARS.size, + "identifierStartChars" to IDENTIFIER_START_CHARS.size, + "identifierBodyChars" to IDENTIFIER_BODY_CHARS.size, + "specialChars" to SPECIAL_CHARS.size + ) + + /** + * ๋ฌธ์ž ์นดํ…Œ๊ณ ๋ฆฌ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class CharCategory { + WHITESPACE, + DIGIT, + LETTER, + OPERATOR, + DELIMITER, + SPECIAL, + VARIABLE_DELIMITER, + STRING_DELIMITER, + COMMENT_START, + UNKNOWN + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/policies/TokenValidationPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/policies/TokenValidationPolicy.kt new file mode 100644 index 00000000..8542b405 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/policies/TokenValidationPolicy.kt @@ -0,0 +1,356 @@ +package hs.kr.entrydsm.domain.lexer.policies + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.lexer.exceptions.LexerException +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ํ† ํฐ ๊ฒ€์ฆ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Policy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ + * ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ํ† ํฐ์˜ ๊ตฌ์กฐ์  ๋ฌด๊ฒฐ์„ฑ, ๊ฐ’์˜ ์œ ํšจ์„ฑ, ํƒ€์ž… ์ผ์น˜์„ฑ ๋“ฑ์„ + * ๊ฒ€์ฆํ•˜์—ฌ ์ž˜๋ชป๋œ ํ† ํฐ์ด ์‹œ์Šคํ…œ์— ์œ ์ž…๋˜๋Š” ๊ฒƒ์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Policy( + name = "TokenValidation", + description = "ํ† ํฐ์˜ ๊ตฌ์กฐ์  ๋ฌด๊ฒฐ์„ฑ๊ณผ ๊ฐ’์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ์ •์ฑ…", + domain = "lexer", + scope = Scope.ENTITY +) +class TokenValidationPolicy { + + companion object { + private const val MAX_TOKEN_LENGTH = 1000 + private const val MAX_NUMBER_VALUE = 1e15 + private const val MIN_NUMBER_VALUE = -1e15 + private const val MAX_IDENTIFIER_LENGTH = 255 + private const val MAX_VARIABLE_NAME_LENGTH = 100 + } + + /** + * ํ† ํฐ์˜ ์ „๋ฐ˜์ ์ธ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ํ† ํฐ + * @return ๊ฒ€์ฆ ํ†ต๊ณผ ์‹œ true + * @throws IllegalArgumentException ๊ฒ€์ฆ ์‹คํŒจ ์‹œ + */ + fun validate(token: Token): Boolean { + validateBasicStructure(token) + validateTypeConsistency(token) + validateValueFormat(token) + validateLength(token) + + return true + } + + /** + * ํ† ํฐ ๋ชฉ๋ก์˜ ์œ ํšจ์„ฑ์„ ์ผ๊ด„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ๋ชจ๋“  ํ† ํฐ์ด ์œ ํšจํ•˜๋ฉด true + */ + fun validateTokens(tokens: List): Boolean { + if (tokens.isEmpty()) { + throw LexerException.tokensEmpty() + } + + tokens.forEach { token -> + validate(token) + } + + validateTokenSequence(tokens) + return true + } + + /** + * ํŠน์ • ํƒ€์ž…์˜ ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ํ† ํฐ + * @param expectedType ๊ธฐ๋Œ€ํ•˜๋Š” ํ† ํฐ ํƒ€์ž… + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateTokenType(token: Token, expectedType: TokenType): Boolean { + if (token.type != expectedType) { + throw LexerException.tokenTypeMismatch(expected = expectedType.name, actual = token.type.name) + } + + validate(token) + return true + } + + /** + * ์ˆซ์ž ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ์ˆซ์ž ํ† ํฐ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateNumberToken(token: Token): Boolean { + if (token.type != TokenType.NUMBER) { + throw LexerException.notNumberToken(token.type.name) // (LEX008 ์žฌ์‚ฌ์šฉ) + } + + val value = try { + token.value.toDouble() + } catch (e: NumberFormatException) { + throw LexerException.invalidNumberFormat(token.value) + } + + val parsed = token.value.toDouble() + if (!parsed.isFinite()) { + throw LexerException.numberNotFinite(parsed) + } + + if (parsed < MIN_NUMBER_VALUE || parsed > MAX_NUMBER_VALUE) { + throw LexerException.numberOutOfRange(parsed, MIN_NUMBER_VALUE, MAX_NUMBER_VALUE) + } + + return true + } + + /** + * ์‹๋ณ„์ž ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ์‹๋ณ„์ž ํ† ํฐ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateIdentifierToken(token: Token): Boolean { + if (token.type != TokenType.IDENTIFIER) { + throw LexerException.notIdentifierToken(token.type.name) + } + + if (token.value.isEmpty()) { + throw LexerException.identifierEmpty() + } + + if (token.value.length > MAX_IDENTIFIER_LENGTH) { + throw LexerException.identifierTooLong(token.value.length, MAX_IDENTIFIER_LENGTH) + } + + if (!token.value.matches(Regex("""^[a-zA-Z_][a-zA-Z0-9_]*$"""))) { + throw LexerException.identifierInvalidFormat(token.value) + } + + return true + } + + /** + * ๋ณ€์ˆ˜ ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ๋ณ€์ˆ˜ ํ† ํฐ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateVariableToken(token: Token): Boolean { + if (token.type != TokenType.VARIABLE) { + throw LexerException.notVariableToken(token.type.name) + } + + if (token.value.isEmpty()) { + throw LexerException.variableNameEmpty(token.value) // (LEX006 ์žฌ์‚ฌ์šฉ) + } + + if (token.value.length > MAX_VARIABLE_NAME_LENGTH) { + throw LexerException.variableNameTooLong(token.value.length, MAX_VARIABLE_NAME_LENGTH) + } + + if (!token.value.matches(Regex("""^[a-zA-Z_][a-zA-Z0-9_]*$"""))) { + throw LexerException.variableNameInvalidFormat(token.value) + } + + return true + } + + /** + * ์—ฐ์‚ฐ์ž ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ์—ฐ์‚ฐ์ž ํ† ํฐ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateOperatorToken(token: Token): Boolean { + if (!token.type.isOperator) { + throw LexerException.notOperatorType(token.type.name) + } + + if (token.value.isEmpty()) { + throw LexerException.operatorValueEmpty() + } + + val validOperators = setOf( + "+", "-", "*", "/", "^", "%", + "==", "!=", "<", "<=", ">", ">=", + "&&", "||", "!" + ) + + if (token.value !in validOperators) { + throw LexerException.unsupportedOperator(token.value) // (LEX013 ์žฌ์‚ฌ์šฉ) + } + + return true + } + + /** + * ํ‚ค์›Œ๋“œ ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ํ‚ค์›Œ๋“œ ํ† ํฐ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateKeywordToken(token: Token): Boolean { + if (!token.type.isKeyword) { + throw LexerException.notKeywordToken(token.type.name) + } + + val validKeywords = mapOf( + TokenType.IF to "if", + TokenType.TRUE to "true", + TokenType.FALSE to "false", + TokenType.AND to "and", + TokenType.OR to "or", + TokenType.NOT to "not" + ) + + val expectedValue = validKeywords[token.type] + + if (!token.value.equals(expectedValue, ignoreCase = true)) { + throw LexerException.keywordValueMismatch(expectedValue, token.value) + } + + return true + } + + /** + * ํ† ํฐ์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateBasicStructure(token: Token) { + if (token.value.length > MAX_TOKEN_LENGTH) { + throw LexerException.tokenTooLong(token.value.length, MAX_TOKEN_LENGTH) + } + } + + /** + * ํ† ํฐ ํƒ€์ž…๊ณผ ๊ฐ’์˜ ์ผ์น˜์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateTypeConsistency(token: Token) { + when (token.type) { + TokenType.NUMBER -> { + if (token.value.toDoubleOrNull() == null) { + throw LexerException.numberTokenNotNumeric(token.value) + } + } + + TokenType.TRUE, TokenType.FALSE -> { + val v = token.value.lowercase() + if (v != "true" && v != "false") { + throw LexerException.booleanTokenInvalid(token.value) // (LEX020 ์žฌ์‚ฌ์šฉ) + } + } + + TokenType.DOLLAR -> { + if (token.value != "$") { + throw LexerException.dollarTokenInvalidValue(token.value) + } + } + else -> { /* ๋‹ค๋ฅธ ํƒ€์ž…๋“ค์€ ์ถ”๊ฐ€ ๊ฒ€์ฆ ์—†์Œ */ } + } + } + + /** + * ํ† ํฐ ๊ฐ’์˜ ํ˜•์‹์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateValueFormat(token: Token) { + when (token.type) { + TokenType.IDENTIFIER, TokenType.VARIABLE -> { + if (!token.value.matches(Regex("""^[a-zA-Z_][a-zA-Z0-9_]*$"""))) { + throw LexerException.invalidIdentifierFormat(token.value) + } + } + TokenType.NUMBER -> { + if (!token.value.matches(Regex("""^-?\d+(\.\d+)?$"""))) { + throw LexerException.invalidNumberFormat(token.value) + } + } + else -> { /* ๋‹ค๋ฅธ ํƒ€์ž…๋“ค์€ ํ˜•์‹ ๊ฒ€์ฆ ์—†์Œ */ } + } + } + + /** + * ํ† ํฐ ๊ธธ์ด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateLength(token: Token) { + when (token.type) { + TokenType.IDENTIFIER -> { + if (token.value.length > MAX_IDENTIFIER_LENGTH) { + throw LexerException.identifierTooLong(token.value.length, MAX_IDENTIFIER_LENGTH) + } + } + + TokenType.VARIABLE -> { + if (token.value.length > MAX_VARIABLE_NAME_LENGTH) { + throw LexerException.variableNameTooLong(token.value.length, MAX_VARIABLE_NAME_LENGTH) + } + } + else -> { /* ๋‹ค๋ฅธ ํƒ€์ž…๋“ค์€ ๊ธธ์ด ์ œํ•œ ์—†์Œ */ } + } + } + + /** + * ํ† ํฐ ์‹œํ€€์Šค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateTokenSequence(tokens: List) { + // ์—ฐ์†๋œ ์—ฐ์‚ฐ์ž ๊ฒ€์ฆ + for (i in 0 until tokens.size - 1) { + val current = tokens[i] + val next = tokens[i + 1] + + if (current.type.isOperator && next.type.isOperator) { + // ์ผ๋ถ€ ์—ฐ์‚ฐ์ž ์กฐํ•ฉ์€ ํ—ˆ์šฉ (์˜ˆ: !, ++) + if (!isValidOperatorSequence(current, next)) { + throw LexerException.invalidOperatorSequence(current.value, next.value) + } + } + } + + // EOF ํ† ํฐ์€ ๋งˆ์ง€๋ง‰์—๋งŒ ์œ„์น˜ํ•ด์•ผ ํ•จ + val eofTokens = tokens.filter { it.type == TokenType.DOLLAR } + if (eofTokens.isNotEmpty()) { + if (eofTokens.size != 1) { + throw LexerException.multipleEofTokens(eofTokens.size) + } + + if (tokens.last().type != TokenType.DOLLAR) { + throw LexerException.eofNotAtEnd() + } + } + } + + /** + * ์—ฐ์‚ฐ์ž ์‹œํ€€์Šค๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidOperatorSequence(first: Token, second: Token): Boolean { + // ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋’ค์—๋Š” ๋‹ค๋ฅธ ์—ฐ์‚ฐ์ž๊ฐ€ ์˜ฌ ์ˆ˜ ์žˆ์Œ + if (first.type in listOf(TokenType.NOT, TokenType.MINUS, TokenType.PLUS)) { + return true + } + + // ๊ธฐํƒ€ ๊ฒฝ์šฐ๋Š” ๋ฌดํšจ + return false + } + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxTokenLength" to MAX_TOKEN_LENGTH, + "maxNumberValue" to MAX_NUMBER_VALUE, + "minNumberValue" to MIN_NUMBER_VALUE, + "maxIdentifierLength" to MAX_IDENTIFIER_LENGTH, + "maxVariableNameLength" to MAX_VARIABLE_NAME_LENGTH + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/policies/TokenizationPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/policies/TokenizationPolicy.kt new file mode 100644 index 00000000..2b224319 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/policies/TokenizationPolicy.kt @@ -0,0 +1,565 @@ +package hs.kr.entrydsm.domain.lexer.policies + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope +import hs.kr.entrydsm.global.constants.ErrorCodes +import hs.kr.entrydsm.global.values.Position + +/** + * POC ์ฝ”๋“œ์˜ CalculatorLexer ๊ธฐ๋Šฅ์„ DDD Policy ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * POC ์ฝ”๋“œ์˜ tokenize ๋ฉ”์„œ๋“œ์—์„œ ์ œ๊ณตํ•˜๋Š” ํ† ํฐํ™” ๊ทœ์น™, ๋ฌธ์ž ์ธ์‹ ์ •์ฑ…, + * ํ‚ค์›Œ๋“œ ๋งคํ•‘, ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๋“ฑ์˜ ๊ธฐ๋Šฅ์„ ์ •์ฑ…์œผ๋กœ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * ์™„์ „ํ•œ ํ† ํฐํ™” ํ”„๋กœ์„ธ์Šค์— ๋Œ€ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.28 + */ +@Policy( + name = "Tokenization", + description = "POC ์ฝ”๋“œ ๊ธฐ๋ฐ˜์˜ ํ† ํฐํ™” ์ฒ˜๋ฆฌ ์ •์ฑ…", + domain = "lexer", + scope = Scope.DOMAIN +) +class TokenizationPolicy { + + companion object { + // POC ์ฝ”๋“œ์˜ CalculatorLexer์—์„œ ์ •์˜๋œ ํ‚ค์›Œ๋“œ๋“ค + private val KEYWORDS = mapOf( + "true" to TokenType.TRUE, + "false" to TokenType.FALSE, + "if" to TokenType.IF, + "PI" to TokenType.VARIABLE, + "E" to TokenType.VARIABLE, + "ABS" to TokenType.IDENTIFIER, + "SQRT" to TokenType.IDENTIFIER, + "ROUND" to TokenType.IDENTIFIER, + "MIN" to TokenType.IDENTIFIER, + "MAX" to TokenType.IDENTIFIER, + "SUM" to TokenType.IDENTIFIER, + "AVG" to TokenType.IDENTIFIER, + "AVERAGE" to TokenType.IDENTIFIER, + "POW" to TokenType.IDENTIFIER, + "LOG" to TokenType.IDENTIFIER, + "LOG10" to TokenType.IDENTIFIER, + "EXP" to TokenType.IDENTIFIER, + "SIN" to TokenType.IDENTIFIER, + "COS" to TokenType.IDENTIFIER, + "TAN" to TokenType.IDENTIFIER, + "ASIN" to TokenType.IDENTIFIER, + "ACOS" to TokenType.IDENTIFIER, + "ATAN" to TokenType.IDENTIFIER, + "ATAN2" to TokenType.IDENTIFIER, + "SINH" to TokenType.IDENTIFIER, + "COSH" to TokenType.IDENTIFIER, + "TANH" to TokenType.IDENTIFIER, + "ASINH" to TokenType.IDENTIFIER, + "ACOSH" to TokenType.IDENTIFIER, + "ATANH" to TokenType.IDENTIFIER, + "FLOOR" to TokenType.IDENTIFIER, + "CEIL" to TokenType.IDENTIFIER, + "CEILING" to TokenType.IDENTIFIER, + "TRUNCATE" to TokenType.IDENTIFIER, + "TRUNC" to TokenType.IDENTIFIER, + "SIGN" to TokenType.IDENTIFIER, + "RANDOM" to TokenType.IDENTIFIER, + "RAND" to TokenType.IDENTIFIER, + "RADIANS" to TokenType.IDENTIFIER, + "DEGREES" to TokenType.IDENTIFIER, + "MOD" to TokenType.IDENTIFIER, + "GCD" to TokenType.IDENTIFIER, + "LCM" to TokenType.IDENTIFIER, + "FACTORIAL" to TokenType.IDENTIFIER, + "COMBINATION" to TokenType.IDENTIFIER, + "COMB" to TokenType.IDENTIFIER, + "PERMUTATION" to TokenType.IDENTIFIER, + "PERM" to TokenType.IDENTIFIER + ) + + // POC ์ฝ”๋“œ์˜ ์—ฐ์‚ฐ์ž ๋งคํ•‘ + private val OPERATORS = mapOf( + '+' to TokenType.PLUS, + '-' to TokenType.MINUS, + '*' to TokenType.MULTIPLY, + '/' to TokenType.DIVIDE, + '%' to TokenType.MODULO, + '^' to TokenType.POWER, + '(' to TokenType.LEFT_PAREN, + ')' to TokenType.RIGHT_PAREN, + ',' to TokenType.COMMA, + '?' to TokenType.IDENTIFIER, + ':' to TokenType.COMMA, + '!' to TokenType.NOT + ) + + // POC ์ฝ”๋“œ์˜ ๋‘ ๋ฌธ์ž ์—ฐ์‚ฐ์ž๋“ค + private val TWO_CHAR_OPERATORS = mapOf( + "==" to TokenType.EQUAL, + "!=" to TokenType.NOT_EQUAL, + "<=" to TokenType.LESS_EQUAL, + ">=" to TokenType.GREATER_EQUAL, + "&&" to TokenType.AND, + "||" to TokenType.OR + ) + + // ํ† ํฐํ™” ์ •์ฑ… ์ƒ์ˆ˜๋“ค + private const val MAX_TOKEN_LENGTH = 1000 + private const val MAX_NUMBER_PRECISION = 15 + private const val MAX_IDENTIFIER_LENGTH = 100 + + private const val POLICY_NAME = "TokenizationPolicy" + private const val BASED_ON = "POC_CalculatorLexer" + private const val VALIDATION_RULES_COUNT = 3 + private const val POC_COMPATIBILITY = true + + // ์„ค์ • ํ‚ค ์ƒ์ˆ˜๋“ค + private const val KEY_NAME = "name" + private const val KEY_BASED_ON = "based_on" + private const val KEY_MAX_TOKEN_LENGTH = "maxTokenLength" + private const val KEY_MAX_NUMBER_PRECISION = "maxNumberPrecision" + private const val KEY_MAX_IDENTIFIER_LENGTH = "maxIdentifierLength" + private const val KEY_SUPPORTED_KEYWORDS = "supportedKeywords" + private const val KEY_SUPPORTED_OPERATORS = "supportedOperators" + private const val KEY_FEATURES = "features" + + // ํ†ต๊ณ„ ํ‚ค ์ƒ์ˆ˜๋“ค + private const val STAT_POLICY_NAME = "policyName" + private const val STAT_KEYWORD_COUNT = "keywordCount" + private const val STAT_SINGLE_CHAR_OPERATOR_COUNT = "singleCharOperatorCount" + private const val STAT_TWO_CHAR_OPERATOR_COUNT = "twoCharOperatorCount" + private const val STAT_SUPPORTED_TOKEN_TYPES = "supportedTokenTypes" + private const val STAT_VALIDATION_RULES = "validationRules" + private const val STAT_POC_COMPATIBILITY = "pocCompatibility" + + // ๊ธฐ๋Šฅ ๋ชฉ๋ก ์ƒ์ˆ˜ + private const val FEATURE_CHARACTER_TOKENIZATION = "character_tokenization" + private const val FEATURE_NUMBER_RECOGNITION = "number_recognition" + private const val FEATURE_IDENTIFIER_RECOGNITION = "identifier_recognition" + private const val FEATURE_OPERATOR_RECOGNITION = "operator_recognition" + private const val FEATURE_SEQUENCE_VALIDATION = "sequence_validation" + private const val FEATURE_QUALITY_EVALUATION = "quality_evaluation" + + private val ALL_FEATURES = listOf( + FEATURE_CHARACTER_TOKENIZATION, + FEATURE_NUMBER_RECOGNITION, + FEATURE_IDENTIFIER_RECOGNITION, + FEATURE_OPERATOR_RECOGNITION, + FEATURE_SEQUENCE_VALIDATION, + FEATURE_QUALITY_EVALUATION + ) + + private fun calculateOperatorCount() = OPERATORS.size + TWO_CHAR_OPERATORS.size + } + + /** + * POC ์ฝ”๋“œ์˜ ๋ฌธ์ž๋ณ„ ํ† ํฐํ™” ์ •์ฑ… ์ ์šฉ + */ + fun applyCharacterTokenizationPolicy(char: Char, position: Int): TokenizationDecision { + return when { + char.isWhitespace() -> TokenizationDecision.SKIP + char.isDigit() -> TokenizationDecision.START_NUMBER + char.isLetter() || char == '_' -> TokenizationDecision.START_IDENTIFIER + char == '.' -> TokenizationDecision.DECIMAL_POINT + char in OPERATORS -> TokenizationDecision.SINGLE_CHAR_OPERATOR + char == '<' || char == '>' -> TokenizationDecision.POSSIBLE_TWO_CHAR_OPERATOR + char == '=' || char == '!' -> TokenizationDecision.POSSIBLE_TWO_CHAR_OPERATOR + char == '&' || char == '|' -> TokenizationDecision.POSSIBLE_TWO_CHAR_OPERATOR + else -> TokenizationDecision.INVALID_CHARACTER + } + } + + /** + * POC ์ฝ”๋“œ์˜ ์ˆซ์ž ํ† ํฐ ์ธ์‹ ์ •์ฑ… + */ + fun applyNumberTokenPolicy(numberString: String, position: Int): NumberTokenResult { + if (numberString.length > MAX_TOKEN_LENGTH) { + return NumberTokenResult.error( + ErrorCodes.Lexer.INVALID_NUMBER_FORMAT.code, + "์ˆซ์ž ํ† ํฐ์ด ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค: ${numberString.length}์ž" + ) + } + + return try { + val value = numberString.toDouble() + + // POC ์ฝ”๋“œ์˜ ์ˆซ์ž ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + when { + !value.isFinite() -> NumberTokenResult.error( + ErrorCodes.Lexer.INVALID_NUMBER_FORMAT.code, + "์œ ํšจํ•˜์ง€ ์•Š์€ ์ˆซ์ž: $numberString" + ) + numberString.count { it == '.' } > 1 -> NumberTokenResult.error( + ErrorCodes.Lexer.INVALID_NUMBER_FORMAT.code, + "์†Œ์ˆ˜์ ์ด ์—ฌ๋Ÿฌ ๊ฐœ ํฌํ•จ๋จ: $numberString" + ) + else -> NumberTokenResult.success( + Token(TokenType.NUMBER, numberString, Position(position, 1, position + 1)) + ) + } + } catch (e: NumberFormatException) { + NumberTokenResult.error( + ErrorCodes.Lexer.INVALID_NUMBER_FORMAT.code, + "์ˆซ์ž ๋ณ€ํ™˜ ์‹คํŒจ: $numberString" + ) + } + } + + /** + * POC ์ฝ”๋“œ์˜ ์‹๋ณ„์ž ํ† ํฐ ์ธ์‹ ์ •์ฑ… + */ + fun applyIdentifierTokenPolicy(identifier: String, position: Int): IdentifierTokenResult { + if (identifier.length > MAX_IDENTIFIER_LENGTH) { + return IdentifierTokenResult.error( + ErrorCodes.Lexer.INVALID_IDENTIFIER.code, + "์‹๋ณ„์ž๊ฐ€ ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค: ${identifier.length}์ž" + ) + } + + if (!isValidIdentifier(identifier)) { + return IdentifierTokenResult.error( + ErrorCodes.Lexer.INVALID_IDENTIFIER.code, + "์œ ํšจํ•˜์ง€ ์•Š์€ ์‹๋ณ„์ž: $identifier" + ) + } + + // POC ์ฝ”๋“œ์˜ ํ‚ค์›Œ๋“œ ๋งคํ•‘ ์ ์šฉ + val tokenType = KEYWORDS[identifier.uppercase()] ?: TokenType.VARIABLE + val token = Token(tokenType, identifier, Position(position, 1, position + 1)) + + return IdentifierTokenResult.success(token) + } + + /** + * POC ์ฝ”๋“œ์˜ ์—ฐ์‚ฐ์ž ํ† ํฐ ์ธ์‹ ์ •์ฑ… + */ + fun applyOperatorTokenPolicy( + currentChar: Char, + nextChar: Char?, + position: Int + ): OperatorTokenResult { + // ๋‘ ๋ฌธ์ž ์—ฐ์‚ฐ์ž ์šฐ์„  ํ™•์ธ + if (nextChar != null) { + val twoCharOp = "$currentChar$nextChar" + TWO_CHAR_OPERATORS[twoCharOp]?.let { tokenType -> + return OperatorTokenResult.success( + Token(tokenType, twoCharOp, Position(position, 1, position + 1)), + consumedChars = 2 + ) + } + } + + // ๋‹จ์ผ ๋ฌธ์ž ์—ฐ์‚ฐ์ž ํ™•์ธ + val tokenType = OPERATORS[currentChar] + return if (tokenType != null) { + OperatorTokenResult.success( + Token(tokenType, currentChar.toString(), Position(position, 1, position + 1)), + consumedChars = 1 + ) + } else { + OperatorTokenResult.error( + ErrorCodes.Lexer.INVALID_CHARACTER.code, + "์ธ์‹๋˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž: $currentChar" + ) + } + } + + /** + * POC ์ฝ”๋“œ์˜ ํ† ํฐ ์‹œํ€€์Šค ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ •์ฑ… + */ + fun applyTokenSequenceValidationPolicy(tokens: List): SequenceValidationResult { + val errors = mutableListOf() + + // ๋นˆ ํ† ํฐ ์‹œํ€€์Šค ๊ฒ€์‚ฌ + if (tokens.isEmpty()) { + errors.add("ํ† ํฐ ์‹œํ€€์Šค๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค") + return SequenceValidationResult(false, errors) + } + + // ์—ฐ์†๋œ ์—ฐ์‚ฐ์ž ๊ฒ€์‚ฌ + for (i in 0 until tokens.size - 1) { + val current = tokens[i] + val next = tokens[i + 1] + + if (isInvalidOperatorSequence(current, next)) { + errors.add("์œ ํšจํ•˜์ง€ ์•Š์€ ์—ฐ์‚ฐ์ž ์‹œํ€€์Šค: ${current.value} ${next.value}") + } + } + + // ๊ด„ํ˜ธ ๊ท ํ˜• ๊ฒ€์‚ฌ + val parenthesesBalance = validateParenthesesBalance(tokens) + if (!parenthesesBalance.isValid) { + errors.addAll(parenthesesBalance.errors) + } + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ตฌ๋ฌธ ๊ฒ€์‚ฌ + val functionValidation = validateFunctionCalls(tokens) + if (!functionValidation.isValid) { + errors.addAll(functionValidation.errors) + } + + return SequenceValidationResult(errors.isEmpty(), errors) + } + + /** + * ํ† ํฐํ™” ํ’ˆ์งˆ ํ‰๊ฐ€ ์ •์ฑ… + */ + fun evaluateTokenizationQuality( + originalInput: String, + tokens: List + ): TokenizationQualityReport { + val report = TokenizationQualityReport() + + // ํ† ํฐ ๋ฐ€๋„ ๊ณ„์‚ฐ + report.tokenDensity = tokens.size.toDouble() / originalInput.length + + // ํ† ํฐ ํƒ€์ž… ๋ถ„ํฌ ๊ณ„์‚ฐ + report.tokenTypeDistribution = tokens.groupBy { it.type } + .mapValues { (_, tokens) -> tokens.size } + + // ๋ณต์žก๋„ ์ ์ˆ˜ ๊ณ„์‚ฐ + report.complexityScore = calculateComplexityScore(tokens) + + // ํŒŒ์‹ฑ ๋‚œ์ด๋„ ํ‰๊ฐ€ + report.parsingDifficulty = assessParsingDifficulty(tokens) + + // ์ตœ์ ํ™” ๊ถŒ์žฅ์‚ฌํ•ญ + report.optimizationRecommendations = generateOptimizationRecommendations(tokens) + + return report + } + + // Private helper methods + + private fun isValidIdentifier(identifier: String): Boolean { + if (identifier.isEmpty()) return false + if (!identifier[0].isLetter() && identifier[0] != '_') return false + + return identifier.all { char -> + char.isLetterOrDigit() || char == '_' + } + } + + private fun isInvalidOperatorSequence(current: Token, next: Token): Boolean { + val binaryOperators = setOf( + TokenType.PLUS, TokenType.MINUS, TokenType.MULTIPLY, TokenType.DIVIDE, + TokenType.MODULO, TokenType.POWER, TokenType.EQUAL, TokenType.NOT_EQUAL, + TokenType.LESS, TokenType.LESS_EQUAL, TokenType.GREATER, + TokenType.GREATER_EQUAL, TokenType.AND, TokenType.OR + ) + + // ์—ฐ์†๋œ ์ดํ•ญ ์—ฐ์‚ฐ์ž๋Š” ํ—ˆ์šฉ๋˜์ง€ ์•Š์Œ (๋‹จํ•ญ ๋งˆ์ด๋„ˆ์Šค ์ œ์™ธ) + return current.type in binaryOperators && + next.type in binaryOperators && + !(current.type == TokenType.LEFT_PAREN && next.type == TokenType.MINUS) + } + + private fun validateParenthesesBalance(tokens: List): ValidationResult { + var balance = 0 + val errors = mutableListOf() + + for ((index, token) in tokens.withIndex()) { + when (token.type) { + TokenType.LEFT_PAREN -> balance++ + TokenType.RIGHT_PAREN -> { + balance-- + if (balance < 0) { + errors.add("์œ„์น˜ ${index}์—์„œ ๋‹ซ๋Š” ๊ด„ํ˜ธ๊ฐ€ ์—ฌ๋Š” ๊ด„ํ˜ธ๋ณด๋‹ค ๋งŽ์Šต๋‹ˆ๋‹ค") + } + } + else -> { /* ๋‹ค๋ฅธ ํ† ํฐ์€ ๋ฌด์‹œ */ } + } + } + + if (balance > 0) { + errors.add("$balance ๊ฐœ์˜ ์—ฌ๋Š” ๊ด„ํ˜ธ๊ฐ€ ๋‹ซํžˆ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค") + } + + return ValidationResult(errors.isEmpty(), errors) + } + + private fun validateFunctionCalls(tokens: List): ValidationResult { + val errors = mutableListOf() + + for (i in 0 until tokens.size - 1) { + val current = tokens[i] + val next = tokens[i + 1] + + if (current.type == TokenType.IDENTIFIER && next.type != TokenType.LEFT_PAREN) { + errors.add("ํ•จ์ˆ˜ ${current.value} ๋‹ค์Œ์— ์—ฌ๋Š” ๊ด„ํ˜ธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค") + } + } + + return ValidationResult(errors.isEmpty(), errors) + } + + private fun calculateComplexityScore(tokens: List): Int { + var score = 0 + + for (token in tokens) { + score += when (token.type) { + TokenType.NUMBER, TokenType.VARIABLE -> 1 + TokenType.PLUS, TokenType.MINUS, TokenType.MULTIPLY, TokenType.DIVIDE -> 2 + TokenType.POWER, TokenType.MODULO -> 3 + TokenType.IDENTIFIER -> 4 + TokenType.IF, TokenType.IDENTIFIER, TokenType.COMMA -> 5 + else -> 1 + } + } + + return score + } + + private fun assessParsingDifficulty(tokens: List): ParsingDifficulty { + val functionCount = tokens.count { it.type == TokenType.IDENTIFIER } + val operatorCount = tokens.count { it.type in setOf( + TokenType.PLUS, TokenType.MINUS, TokenType.MULTIPLY, TokenType.DIVIDE, + TokenType.POWER, TokenType.MODULO + )} + val conditionalCount = tokens.count { it.type in setOf(TokenType.IF, TokenType.IDENTIFIER) } + + return when { + functionCount > 5 || conditionalCount > 2 -> ParsingDifficulty.VERY_HARD + functionCount > 2 || operatorCount > 10 -> ParsingDifficulty.HARD + operatorCount > 5 -> ParsingDifficulty.MEDIUM + else -> ParsingDifficulty.EASY + } + } + + private fun generateOptimizationRecommendations(tokens: List): List { + val recommendations = mutableListOf() + + val functionTokens = tokens.filter { it.type == TokenType.IDENTIFIER } + if (functionTokens.size > 10) { + recommendations.add("ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด ๋งŽ์Šต๋‹ˆ๋‹ค (${functionTokens.size}๊ฐœ). ์ค‘๊ฐ„ ๊ฒฐ๊ณผ๋ฅผ ๋ณ€์ˆ˜์— ์ €์žฅํ•˜๋Š” ๊ฒƒ์„ ๊ณ ๋ คํ•˜์„ธ์š”.") + } + + val powerOperations = tokens.count { it.type == TokenType.POWER } + if (powerOperations > 3) { + recommendations.add("๊ฑฐ๋“ญ์ œ๊ณฑ ์—ฐ์‚ฐ์ด ๋งŽ์Šต๋‹ˆ๋‹ค ($powerOperations ๊ฐœ). ์„ฑ๋Šฅ์— ์˜ํ–ฅ์„ ์ค„ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.") + } + + val parenthesesDepth = calculateMaxParenthesesDepth(tokens) + if (parenthesesDepth > 5) { + recommendations.add("๊ด„ํ˜ธ ์ค‘์ฒฉ์ด ๊นŠ์Šต๋‹ˆ๋‹ค (๊นŠ์ด: $parenthesesDepth). ๊ฐ€๋…์„ฑ์„ ์œ„ํ•ด ๋‹จ์ˆœํ™”๋ฅผ ๊ณ ๋ คํ•˜์„ธ์š”.") + } + + return recommendations + } + + private fun calculateMaxParenthesesDepth(tokens: List): Int { + var maxDepth = 0 + var currentDepth = 0 + + for (token in tokens) { + when (token.type) { + TokenType.LEFT_PAREN -> { + currentDepth++ + maxDepth = maxOf(maxDepth, currentDepth) + } + TokenType.RIGHT_PAREN -> currentDepth-- + else -> { /* ๋ฌด์‹œ */ } + } + } + + return maxDepth + } + + // Enums and Data Classes + + enum class TokenizationDecision { + SKIP, START_NUMBER, START_IDENTIFIER, DECIMAL_POINT, + SINGLE_CHAR_OPERATOR, POSSIBLE_TWO_CHAR_OPERATOR, INVALID_CHARACTER + } + + enum class ParsingDifficulty { + EASY, MEDIUM, HARD, VERY_HARD + } + + data class NumberTokenResult( + val success: Boolean, + val token: Token? = null, + val errorCode: String? = null, + val errorMessage: String? = null + ) { + companion object { + fun success(token: Token) = NumberTokenResult(true, token) + fun error(code: String, message: String) = NumberTokenResult(false, null, code, message) + } + } + + data class IdentifierTokenResult( + val success: Boolean, + val token: Token? = null, + val errorCode: String? = null, + val errorMessage: String? = null + ) { + companion object { + fun success(token: Token) = IdentifierTokenResult(true, token) + fun error(code: String, message: String) = IdentifierTokenResult(false, null, code, message) + } + } + + data class OperatorTokenResult( + val success: Boolean, + val token: Token? = null, + val consumedChars: Int = 1, + val errorCode: String? = null, + val errorMessage: String? = null + ) { + companion object { + fun success(token: Token, consumedChars: Int) = + OperatorTokenResult(true, token, consumedChars) + fun error(code: String, message: String) = + OperatorTokenResult(false, null, 1, code, message) + } + } + + data class ValidationResult( + val isValid: Boolean, + val errors: List = emptyList() + ) + + data class SequenceValidationResult( + val isValid: Boolean, + val errors: List = emptyList() + ) + + data class TokenizationQualityReport( + var tokenDensity: Double = 0.0, + var tokenTypeDistribution: Map = emptyMap(), + var complexityScore: Int = 0, + var parsingDifficulty: ParsingDifficulty = ParsingDifficulty.EASY, + var optimizationRecommendations: List = emptyList() + ) + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getConfiguration(): Map = mapOf( + KEY_NAME to POLICY_NAME, + KEY_BASED_ON to BASED_ON, + KEY_MAX_TOKEN_LENGTH to MAX_TOKEN_LENGTH, + KEY_MAX_NUMBER_PRECISION to MAX_NUMBER_PRECISION, + KEY_MAX_IDENTIFIER_LENGTH to MAX_IDENTIFIER_LENGTH, + KEY_SUPPORTED_KEYWORDS to KEYWORDS.size, + KEY_SUPPORTED_OPERATORS to calculateOperatorCount(), + KEY_FEATURES to ALL_FEATURES + ) + + /** + * ์ •์ฑ…์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getStatistics(): Map = mapOf( + STAT_POLICY_NAME to POLICY_NAME, + STAT_KEYWORD_COUNT to KEYWORDS.size, + STAT_SINGLE_CHAR_OPERATOR_COUNT to OPERATORS.size, + STAT_TWO_CHAR_OPERATOR_COUNT to TWO_CHAR_OPERATORS.size, + STAT_SUPPORTED_TOKEN_TYPES to TokenType.values().size, + STAT_VALIDATION_RULES to VALIDATION_RULES_COUNT, + STAT_POC_COMPATIBILITY to POC_COMPATIBILITY + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/specifications/InputValiditySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/specifications/InputValiditySpec.kt new file mode 100644 index 00000000..eb84c846 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/specifications/InputValiditySpec.kt @@ -0,0 +1,381 @@ +package hs.kr.entrydsm.domain.lexer.specifications + +import hs.kr.entrydsm.domain.lexer.exceptions.LexerException +import hs.kr.entrydsm.domain.lexer.values.LexingContext +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * ์ž…๋ ฅ ํ…์ŠคํŠธ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” Specification ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Specification ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์–ดํœ˜ ๋ถ„์„ ์ „ ์ž…๋ ฅ ๋ฐ์ดํ„ฐ์˜ + * ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์ž…๋ ฅ ๊ธธ์ด, ๋ฌธ์ž ์ง‘ํ•ฉ, + * ์ธ์ฝ”๋”ฉ, ๊ตฌ์กฐ์  ์ œ์•ฝ ๋“ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Specification( + name = "InputValidity", + description = "์–ดํœ˜ ๋ถ„์„ ์ž…๋ ฅ ํ…์ŠคํŠธ์˜ ์œ ํšจ์„ฑ๊ณผ ์ œ์•ฝ ์กฐ๊ฑด์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ", + domain = "lexer", + priority = Priority.HIGH +) +class InputValiditySpec { + + companion object { + private const val MAX_INPUT_LENGTH = 1_000_000 // 1MB + private const val MAX_LINE_LENGTH = 10_000 + private const val MAX_LINE_COUNT = 50_000 + private const val MAX_TOKEN_LENGTH = 1_000 + private const val MAX_NESTING_DEPTH = 100 + + // ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ์ œ์–ด ๋ฌธ์ž๋“ค (์ผ๋ถ€ ์ œ์™ธ) + private val FORBIDDEN_CONTROL_CHARS = (0x00..0x1F).toSet() - setOf( + 0x09, // TAB + 0x0A, // LF (Line Feed) + 0x0D // CR (Carriage Return) + ) + } + + private var strictMode: Boolean = true + private var allowUnicode: Boolean = false + private var allowExtendedASCII: Boolean = false + private var maxInputLength: Int = MAX_INPUT_LENGTH + private var maxLineLength: Int = MAX_LINE_LENGTH + private var maxLineCount: Int = MAX_LINE_COUNT + + /** + * ๋ช…์„ธ ์„ค์ •์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param strictMode ์—„๊ฒฉ ๋ชจ๋“œ ์—ฌ๋ถ€ + * @param allowUnicode ์œ ๋‹ˆ์ฝ”๋“œ ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @param allowExtendedASCII ํ™•์žฅ ASCII ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @param maxInputLength ์ตœ๋Œ€ ์ž…๋ ฅ ๊ธธ์ด + * @param maxLineLength ์ตœ๋Œ€ ๋ผ์ธ ๊ธธ์ด + * @param maxLineCount ์ตœ๋Œ€ ๋ผ์ธ ์ˆ˜ + */ + fun configure( + strictMode: Boolean = true, + allowUnicode: Boolean = false, + allowExtendedASCII: Boolean = false, + maxInputLength: Int = MAX_INPUT_LENGTH, + maxLineLength: Int = MAX_LINE_LENGTH, + maxLineCount: Int = MAX_LINE_COUNT + ) { + this.strictMode = strictMode + this.allowUnicode = allowUnicode + this.allowExtendedASCII = allowExtendedASCII + this.maxInputLength = maxInputLength + this.maxLineLength = maxLineLength + this.maxLineCount = maxLineCount + } + + /** + * ์ž…๋ ฅ ํ…์ŠคํŠธ๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ๊ฒ€์ฆํ•  ์ž…๋ ฅ ํ…์ŠคํŠธ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isSatisfiedBy(input: String): Boolean { + return hasValidLength(input) && + hasValidCharacterSet(input) && + hasValidLineStructure(input) && + hasValidEncoding(input) && + hasValidNestingDepth(input) && + hasNoForbiddenPatterns(input) + } + + /** + * LexingContext๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param context ๊ฒ€์ฆํ•  ์ปจํ…์ŠคํŠธ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isValidContext(context: LexingContext): Boolean { + return isSatisfiedBy(context.input) && + hasValidPosition(context) && + hasValidConfiguration(context) + } + + /** + * ์ž…๋ ฅ์ด ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ํ™•์ธํ•  ์ž…๋ ฅ + * @return ๋น„์–ด์žˆ์œผ๋ฉด true + */ + fun isEmpty(input: String): Boolean { + return input.isEmpty() + } + + /** + * ์ž…๋ ฅ์ด ๊ณต๋ฐฑ๋งŒ ํฌํ•จํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ํ™•์ธํ•  ์ž…๋ ฅ + * @return ๊ณต๋ฐฑ๋งŒ ํฌํ•จํ•˜๋ฉด true + */ + fun isBlank(input: String): Boolean { + return input.isBlank() + } + + /** + * ์ž…๋ ฅ์ด ๋‹จ์ผ ๋ผ์ธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ํ™•์ธํ•  ์ž…๋ ฅ + * @return ๋‹จ์ผ ๋ผ์ธ์ด๋ฉด true + */ + fun isSingleLine(input: String): Boolean { + return !input.contains('\n') && !input.contains('\r') + } + + /** + * ์ž…๋ ฅ์ด ASCII ์ „์šฉ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ํ™•์ธํ•  ์ž…๋ ฅ + * @return ASCII ์ „์šฉ์ด๋ฉด true + */ + fun isASCIIOnly(input: String): Boolean { + return input.all { it.code <= 127 } + } + + /** + * ์ž…๋ ฅ์— ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ํ™•์ธํ•  ์ž…๋ ฅ + * @return ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด true + */ + fun hasUnicodeChars(input: String): Boolean { + return input.any { it.code > 127 } + } + + /** + * ์ž…๋ ฅ์ด ์œ ํšจํ•œ ๊ธธ์ด๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidLength(input: String): Boolean { + if (input.length > maxInputLength) { + throw LexerException.inputLengthExceeded(input.length, maxInputLength) + } + + return true + } + + /** + * ์ž…๋ ฅ์ด ์œ ํšจํ•œ ๋ฌธ์ž ์ง‘ํ•ฉ์„ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidCharacterSet(input: String): Boolean { + for (char in input) { + val codePoint = char.code + + when { + codePoint <= 127 -> continue // ASCII + allowExtendedASCII && codePoint <= 255 -> continue // ํ™•์žฅ ASCII + allowUnicode -> continue // ์œ ๋‹ˆ์ฝ”๋“œ + else -> throw LexerException.disallowedCharacter(char, codePoint) + } + + // ๊ธˆ์ง€๋œ ์ œ์–ด ๋ฌธ์ž ๊ฒ€์‚ฌ + if (codePoint in FORBIDDEN_CONTROL_CHARS) { + throw LexerException.forbiddenControlCharacter(codePoint) + } + } + return true + } + + /** + * ์ž…๋ ฅ์ด ์œ ํšจํ•œ ๋ผ์ธ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidLineStructure(input: String): Boolean { + val lines = input.split('\n', '\r') + + if (lines.size > maxLineCount) { + throw LexerException.lineCountExceeded(lines.size, maxLineCount) + } + + lines.forEachIndexed { index, line -> + if (line.length > maxLineLength) { + throw LexerException.lineLengthExceeded(index, line.length, maxLineLength) + } + } + + return true + } + + /** + * ์ž…๋ ฅ์ด ์œ ํšจํ•œ ์ธ์ฝ”๋”ฉ์„ ๊ฐ€์ง€๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidEncoding(input: String): Boolean { + // BOM (Byte Order Mark) ๊ฒ€์‚ฌ + if (input.startsWith('\uFEFF')) { + if (strictMode) { + throw LexerException.bomCharacterDetected() + } + } + + // ๋„ ๋ฌธ์ž ๊ฒ€์‚ฌ + if (input.contains('\u0000')) { + throw LexerException.nullCharacterDetected() + } + + return true + } + + /** + * ์ž…๋ ฅ์ด ์œ ํšจํ•œ ์ค‘์ฒฉ ๊นŠ์ด๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidNestingDepth(input: String): Boolean { + var depth = 0 + var maxDepth = 0 + + for (char in input) { + when (char) { + '(', '{', '[' -> { + depth++ + maxDepth = maxOf(maxDepth, depth) + } + ')', '}', ']' -> { + depth-- + } + } + + if (maxDepth > MAX_NESTING_DEPTH) { + throw LexerException.maxNestingDepthExceeded(maxDepth, MAX_NESTING_DEPTH) + } + } + + return true + } + + /** + * ์ž…๋ ฅ์— ๊ธˆ์ง€๋œ ํŒจํ„ด์ด ์—†๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasNoForbiddenPatterns(input: String): Boolean { + // ์—ฐ์†๋œ ๊ณต๋ฐฑ์ด ๋„ˆ๋ฌด ๋งŽ์€ ๊ฒฝ์šฐ + if (input.contains(Regex("\\s{100,}"))) { + if (strictMode) { + throw LexerException.excessiveWhitespaceDetected() + } + } + + // ์˜์‹ฌ์Šค๋Ÿฌ์šด ๋ฐ˜๋ณต ํŒจํ„ด + if (input.contains(Regex("(.{1,10})\\1{50,}"))) { + if (strictMode) { + throw LexerException.suspiciousRepeatPattern() + } + } + + return true + } + + /** + * ์ปจํ…์ŠคํŠธ์˜ ์œ„์น˜๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidPosition(context: LexingContext): Boolean { + val position = context.currentPosition + + if (position.index < 0 || position.index > context.input.length) { + throw LexerException.invalidPositionIndex(position.index, context.input.length) + } + + if (position.line < 1) { + throw LexerException.invalidPositionLine(position.line) + } + + if (position.column < 1) { + throw LexerException.invalidPositionColumn(position.column) + } + + return true + } + + /** + * ์ปจํ…์ŠคํŠธ์˜ ์„ค์ •์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidConfiguration(context: LexingContext): Boolean { + if (context.maxTokenLength <= 0) { + throw LexerException.invalidMaxTokenLength(context.maxTokenLength) + } + + if (context.maxTokenLength > MAX_TOKEN_LENGTH) { + throw LexerException.maxTokenLengthExceeded(context.maxTokenLength, MAX_TOKEN_LENGTH) + } + + return true + } + + /** + * ์ž…๋ ฅ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ๋ถ„์„ํ•  ์ž…๋ ฅ + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getInputStatistics(input: String): Map { + val lines = input.split('\n', '\r') + + return mapOf( + "totalLength" to input.length, + "lineCount" to lines.size, + "maxLineLength" to (lines.maxOfOrNull { it.length } ?: 0), + "avgLineLength" to if (lines.isNotEmpty()) input.length / lines.size else 0, + "hasUnicode" to hasUnicodeChars(input), + "isASCIIOnly" to isASCIIOnly(input), + "isSingleLine" to isSingleLine(input), + "isEmpty" to isEmpty(input), + "isBlank" to isBlank(input), + "whitespaceRatio" to if (input.isNotEmpty()) + input.count { it.isWhitespace() }.toDouble() / input.length else 0.0 + ) + } + + /** + * ๋ช…์„ธ์˜ ํ˜„์žฌ ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "strictMode" to strictMode, + "allowUnicode" to allowUnicode, + "allowExtendedASCII" to allowExtendedASCII, + "maxInputLength" to maxInputLength, + "maxLineLength" to maxLineLength, + "maxLineCount" to maxLineCount, + "maxTokenLength" to MAX_TOKEN_LENGTH, + "maxNestingDepth" to MAX_NESTING_DEPTH + ) + + /** + * ์ž…๋ ฅ์˜ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๊ฒฐ๊ณผ์™€ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ๊ฒ€์ฆํ•  ์ž…๋ ฅ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ ๋งต (isValid, errors, warnings, statistics) + */ + fun validateWithDetails(input: String): Map { + val errors = mutableListOf() + val warnings = mutableListOf() + var isValid = true + + try { + isSatisfiedBy(input) + } catch (e: IllegalArgumentException) { + errors.add(e.message ?: "์•Œ ์ˆ˜ ์—†๋Š” ๊ฒ€์ฆ ์˜ค๋ฅ˜") + isValid = false + } + + // ๊ฒฝ๊ณ  ์ˆ˜์ค€์˜ ๊ฒ€์‚ฌ๋“ค + if (input.length > maxInputLength * 0.8) { + warnings.add("์ž…๋ ฅ ๊ธธ์ด๊ฐ€ ๊ถŒ์žฅ ํฌ๊ธฐ์— ๊ทผ์ ‘ํ–ˆ์Šต๋‹ˆ๋‹ค") + } + + if (hasUnicodeChars(input) && !allowUnicode) { + warnings.add("์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์ง€๋งŒ ํ—ˆ์šฉ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") + } + + return mapOf( + "isValid" to isValid, + "errors" to errors, + "warnings" to warnings, + "statistics" to getInputStatistics(input) + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/specifications/TokenValidationSpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/specifications/TokenValidationSpec.kt new file mode 100644 index 00000000..4f24fefa --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/specifications/TokenValidationSpec.kt @@ -0,0 +1,368 @@ +package hs.kr.entrydsm.domain.lexer.specifications + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * ํ† ํฐ ๊ฒ€์ฆ์„ ์œ„ํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์ •์˜ํ•˜๋Š” Specification ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Specification ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ† ํฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ณต์žกํ•œ + * ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ  ์กฐํ•ฉ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ๋กœ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๋‹ค์–‘ํ•œ ํ† ํฐ ๊ฒ€์ฆ ๊ทœ์น™์„ ๋…๋ฆฝ์ ์œผ๋กœ ์ •์˜ํ•˜๊ณ  ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Specification( + name = "TokenValidation", + description = "ํ† ํฐ์˜ ๊ตฌ์กฐ์  ๋ฌด๊ฒฐ์„ฑ๊ณผ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ์ค€์ˆ˜๋ฅผ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ", + domain = "lexer", + priority = Priority.HIGH +) +class TokenValidationSpec { + + companion object { + // ์ŠคํŽ™ ๊ด€๋ จ ์ƒ์ˆ˜๋“ค + private const val SPEC_NAME = "TokenValidationSpec" + private const val MAX_IDENTIFIER_LENGTH_VALUE = 255 + private const val MAX_VARIABLE_LENGTH_VALUE = 100 + private const val MAX_NUMBER_LENGTH_VALUE = 50 + + // ์ŠคํŽ™ ํ‚ค ์ƒ์ˆ˜๋“ค + private const val SPEC_KEY_NAME = "name" + private const val SPEC_KEY_SUPPORTED_TOKEN_TYPES = "supportedTokenTypes" + private const val SPEC_KEY_VALIDATION_RULES = "validationRules" + private const val SPEC_KEY_MAX_IDENTIFIER_LENGTH = "maxIdentifierLength" + private const val SPEC_KEY_MAX_VARIABLE_LENGTH = "maxVariableLength" + private const val SPEC_KEY_MAX_NUMBER_LENGTH = "maxNumberLength" + + // ๊ฒ€์ฆ ๊ทœ์น™ ์ƒ์ˆ˜๋“ค + private const val RULE_HAS_VALID_STRUCTURE = "hasValidStructure" + private const val RULE_HAS_CONSISTENT_TYPE_AND_VALUE = "hasConsistentTypeAndValue" + private const val RULE_HAS_VALID_LENGTH = "hasValidLength" + private const val RULE_FOLLOWS_NAMING_CONVENTIONS = "followsNamingConventions" + + private val ALL_VALIDATION_RULES = listOf( + RULE_HAS_VALID_STRUCTURE, + RULE_HAS_CONSISTENT_TYPE_AND_VALUE, + RULE_HAS_VALID_LENGTH, + RULE_FOLLOWS_NAMING_CONVENTIONS + ) + } + + /** + * ํ† ํฐ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ํ† ํฐ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isSatisfiedBy(token: Token): Boolean { + return hasValidStructure(token) && + hasConsistentTypeAndValue(token) && + hasValidLength(token) && + followsNamingConventions(token) + } + + /** + * ํ† ํฐ์ด ํŠน์ • ํƒ€์ž…์˜ ์œ ํšจํ•œ ํ† ํฐ์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ํ† ํฐ + * @param expectedType ๊ธฐ๋Œ€ํ•˜๋Š” ํ† ํฐ ํƒ€์ž… + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isValidTokenOfType(token: Token, expectedType: TokenType): Boolean { + return token.type == expectedType && isSatisfiedBy(token) + } + + /** + * ํ† ํฐ์ด ์œ ํšจํ•œ ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ํ† ํฐ + * @return ์œ ํšจํ•œ ๋ฆฌํ„ฐ๋Ÿด์ด๋ฉด true + */ + fun isValidLiteral(token: Token): Boolean { + if (!token.type.isLiteral) return false + + return when (token.type) { + TokenType.NUMBER -> isValidNumberLiteral(token) + TokenType.TRUE, TokenType.FALSE -> isValidBooleanLiteral(token) + TokenType.IDENTIFIER -> isValidIdentifierLiteral(token) + TokenType.VARIABLE -> isValidVariableLiteral(token) + else -> false + } + } + + /** + * ํ† ํฐ์ด ์œ ํšจํ•œ ์—ฐ์‚ฐ์ž์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ํ† ํฐ + * @return ์œ ํšจํ•œ ์—ฐ์‚ฐ์ž์ด๋ฉด true + */ + fun isValidOperator(token: Token): Boolean { + if (!token.type.isOperator) return false + + return when (token.type) { + TokenType.PLUS, TokenType.MINUS -> isValidArithmeticOperator(token) + TokenType.MULTIPLY, TokenType.DIVIDE, TokenType.POWER, TokenType.MODULO -> + isValidArithmeticOperator(token) + TokenType.EQUAL, TokenType.NOT_EQUAL -> isValidComparisonOperator(token) + TokenType.LESS, TokenType.LESS_EQUAL, TokenType.GREATER, TokenType.GREATER_EQUAL -> + isValidComparisonOperator(token) + TokenType.AND, TokenType.OR, TokenType.NOT -> isValidLogicalOperator(token) + else -> false + } + } + + /** + * ํ† ํฐ์ด ์œ ํšจํ•œ ํ‚ค์›Œ๋“œ์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ํ† ํฐ + * @return ์œ ํšจํ•œ ํ‚ค์›Œ๋“œ์ด๋ฉด true + */ + fun isValidKeyword(token: Token): Boolean { + if (!token.type.isKeyword) return false + + val expectedValues = mapOf( + TokenType.IF to setOf("if"), + TokenType.TRUE to setOf("true"), + TokenType.FALSE to setOf("false"), + TokenType.AND to setOf("and", "&&"), + TokenType.OR to setOf("or", "||"), + TokenType.NOT to setOf("not", "!") + ) + + val allowedValues = expectedValues[token.type] ?: return false + return allowedValues.any { it.equals(token.value, ignoreCase = true) } + } + + /** + * ํ† ํฐ์ด ์œ ํšจํ•œ ๊ตฌ๋ถ„์ž์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฒ€์ฆํ•  ํ† ํฐ + * @return ์œ ํšจํ•œ ๊ตฌ๋ถ„์ž์ด๋ฉด true + */ + fun isValidDelimiter(token: Token): Boolean { + val validDelimiters = mapOf( + TokenType.LEFT_PAREN to "(", + TokenType.RIGHT_PAREN to ")", + TokenType.COMMA to "," + ) + + return validDelimiters[token.type] == token.value + } + + /** + * ํ† ํฐ์ด ๋น„์–ด์žˆ์ง€ ์•Š์€ ์œ ํšจํ•œ ๊ตฌ์กฐ๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidStructure(token: Token): Boolean { + return token.value.isNotEmpty() || token.type == TokenType.DOLLAR + } + + /** + * ํ† ํฐ ํƒ€์ž…๊ณผ ๊ฐ’์ด ์ผ๊ด€์„ฑ์„ ๊ฐ€์ง€๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasConsistentTypeAndValue(token: Token): Boolean { + return when (token.type) { + TokenType.NUMBER -> token.value.toDoubleOrNull() != null + TokenType.TRUE -> token.value.equals("true", ignoreCase = true) + TokenType.FALSE -> token.value.equals("false", ignoreCase = true) + TokenType.DOLLAR -> token.value == "$" + TokenType.LEFT_PAREN -> token.value == "(" + TokenType.RIGHT_PAREN -> token.value == ")" + TokenType.COMMA -> token.value == "," + else -> true // ๋‹ค๋ฅธ ํƒ€์ž…๋“ค์€ ๋ณ„๋„ ๊ฒ€์ฆ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ + } + } + + /** + * ํ† ํฐ์ด ์ ์ ˆํ•œ ๊ธธ์ด๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidLength(token: Token): Boolean { + val maxLengths = mapOf( + TokenType.IDENTIFIER to 255, + TokenType.VARIABLE to 100, + TokenType.NUMBER to 50 + ) + + val maxLength = maxLengths[token.type] ?: 1000 + return token.value.length <= maxLength + } + + /** + * ํ† ํฐ์ด ๋ช…๋ช… ๊ทœ์น™์„ ๋”ฐ๋ฅด๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun followsNamingConventions(token: Token): Boolean { + return when (token.type) { + TokenType.IDENTIFIER, TokenType.VARIABLE -> + token.value.matches(Regex("^[a-zA-Z_][a-zA-Z0-9_]*$")) + else -> true + } + } + + /** + * ์œ ํšจํ•œ ์ˆซ์ž ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidNumberLiteral(token: Token): Boolean { + return try { + val value = token.value.toDouble() + value.isFinite() && + token.value.matches(Regex("^-?\\d+(\\.\\d+)?([eE][+-]?\\d+)?$")) + } catch (e: NumberFormatException) { + false + } + } + + /** + * ์œ ํšจํ•œ ๋ถˆ๋ฆฐ ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidBooleanLiteral(token: Token): Boolean { + return token.value.lowercase() in setOf("true", "false") + } + + /** + * ์œ ํšจํ•œ ์‹๋ณ„์ž ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidIdentifierLiteral(token: Token): Boolean { + return token.value.isNotEmpty() && + token.value.matches(Regex("^[a-zA-Z_][a-zA-Z0-9_]*$")) && + !isReservedWord(token.value) + } + + /** + * ์œ ํšจํ•œ ๋ณ€์ˆ˜ ๋ฆฌํ„ฐ๋Ÿด์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidVariableLiteral(token: Token): Boolean { + return token.value.isNotEmpty() && + token.value.matches(Regex("^[a-zA-Z_][a-zA-Z0-9_]*$")) && + token.value.length <= 100 + } + + /** + * ์œ ํšจํ•œ ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidArithmeticOperator(token: Token): Boolean { + val validOperators = setOf("+", "-", "*", "/", "^", "%") + return token.value in validOperators + } + + /** + * ์œ ํšจํ•œ ๋น„๊ต ์—ฐ์‚ฐ์ž์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidComparisonOperator(token: Token): Boolean { + val validOperators = setOf("==", "!=", "<", "<=", ">", ">=") + return token.value in validOperators + } + + /** + * ์œ ํšจํ•œ ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidLogicalOperator(token: Token): Boolean { + val validOperators = setOf("&&", "||", "!", "and", "or", "not") + return token.value.lowercase() in validOperators + } + + /** + * ์˜ˆ์•ฝ์–ด์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isReservedWord(value: String): Boolean { + val reservedWords = setOf( + "if", "then", "else", "endif", + "true", "false", + "and", "or", "not", + "mod", "div", + "let", "var", "const", + "function", "return", + "while", "for", "break", "continue" + ) + return value.lowercase() in reservedWords + } + + /** + * ํ† ํฐ ๋ชฉ๋ก์˜ ์ „์ฒด์ ์ธ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ๋ชจ๋“  ํ† ํฐ์ด ์œ ํšจํ•˜๋ฉด true + */ + fun areAllTokensValid(tokens: List): Boolean { + return tokens.all { isSatisfiedBy(it) } && + hasValidTokenSequence(tokens) + } + + /** + * ํ† ํฐ ์‹œํ€€์Šค๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidTokenSequence(tokens: List): Boolean { + if (tokens.isEmpty()) return true + + // ์—ฐ์†๋œ ์—ฐ์‚ฐ์ž ๊ฒ€์ฆ + for (i in 0 until tokens.size - 1) { + val current = tokens[i] + val next = tokens[i + 1] + + if (current.type.isOperator && next.type.isOperator) { + if (!isValidOperatorSequence(current, next)) { + return false + } + } + } + + // ๊ด„ํ˜ธ ๊ท ํ˜• ๊ฒ€์ฆ + if (!hasBalancedParentheses(tokens)) { + return false + } + + // EOF ํ† ํฐ ์œ„์น˜ ๊ฒ€์ฆ + val eofTokens = tokens.filter { it.type == TokenType.DOLLAR } + if (eofTokens.size > 1) return false + if (eofTokens.size == 1 && tokens.last().type != TokenType.DOLLAR) return false + + return true + } + + /** + * ์—ฐ์†๋œ ์—ฐ์‚ฐ์ž๊ฐ€ ์œ ํšจํ•œ ์กฐํ•ฉ์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidOperatorSequence(first: Token, second: Token): Boolean { + // ๋‹จํ•ญ ์—ฐ์‚ฐ์ž(!, -, +) ๋’ค์—๋Š” ๋‹ค๋ฅธ ์—ฐ์‚ฐ์ž๊ฐ€ ์˜ฌ ์ˆ˜ ์žˆ์Œ + val unaryOperators = setOf(TokenType.NOT, TokenType.MINUS, TokenType.PLUS) + return first.type in unaryOperators + } + + /** + * ๊ด„ํ˜ธ๊ฐ€ ๊ท ํ˜•์„ ์ด๋ฃจ๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasBalancedParentheses(tokens: List): Boolean { + var balance = 0 + + for (token in tokens) { + when (token.type) { + TokenType.LEFT_PAREN -> balance++ + TokenType.RIGHT_PAREN -> { + balance-- + if (balance < 0) return false + } + else -> { /* ๋‹ค๋ฅธ ํ† ํฐ์€ ๋ฌด์‹œ */ } + } + } + + return balance == 0 + } + + /** + * ๋ช…์„ธ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getSpecificationInfo(): Map = mapOf( + SPEC_KEY_NAME to SPEC_NAME, + SPEC_KEY_SUPPORTED_TOKEN_TYPES to TokenType.values().map { it.name }, + SPEC_KEY_VALIDATION_RULES to ALL_VALIDATION_RULES, + SPEC_KEY_MAX_IDENTIFIER_LENGTH to MAX_IDENTIFIER_LENGTH_VALUE, + SPEC_KEY_MAX_VARIABLE_LENGTH to MAX_VARIABLE_LENGTH_VALUE, + SPEC_KEY_MAX_NUMBER_LENGTH to MAX_NUMBER_LENGTH_VALUE + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/values/LexingContext.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/values/LexingContext.kt new file mode 100644 index 00000000..2be1b910 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/values/LexingContext.kt @@ -0,0 +1,322 @@ +package hs.kr.entrydsm.domain.lexer.values + +import hs.kr.entrydsm.domain.lexer.exceptions.LexerException +import hs.kr.entrydsm.global.values.Position + +/** + * ์–ดํœ˜ ๋ถ„์„ ์ปจํ…์ŠคํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * Lexer๊ฐ€ ํ…์ŠคํŠธ๋ฅผ ๋ถ„์„ํ•˜๋Š” ๋™์•ˆ ์œ ์ง€ํ•ด์•ผ ํ•˜๋Š” ์ƒํƒœ ์ •๋ณด๋ฅผ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * ํ˜„์žฌ ์œ„์น˜, ์ž…๋ ฅ ํ…์ŠคํŠธ, ๋ถ„์„ ์˜ต์…˜ ๋“ฑ์„ ํฌํ•จํ•˜๋ฉฐ, ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋˜์–ด + * ์•ˆ์ „ํ•œ ์ƒํƒœ ์ „๋‹ฌ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @property input ๋ถ„์„ํ•  ์ž…๋ ฅ ํ…์ŠคํŠธ + * @property currentPosition ํ˜„์žฌ ๋ถ„์„ ์œ„์น˜ + * @property startTime ๋ถ„์„ ์‹œ์ž‘ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + * @property strictMode ์—„๊ฒฉ ๋ชจ๋“œ ์—ฌ๋ถ€ (์—๋Ÿฌ ํ—ˆ์šฉ๋„) + * @property skipWhitespace ๊ณต๋ฐฑ ๋ฌธ์ž ์Šคํ‚ต ์—ฌ๋ถ€ + * @property allowUnicode ์œ ๋‹ˆ์ฝ”๋“œ ๋ฌธ์ž ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @property maxTokenLength ์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด ์ œํ•œ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +data class LexingContext( + val input: String, + val currentPosition: Position = Position.START, + val startTime: Long = System.currentTimeMillis(), + val strictMode: Boolean = true, + val skipWhitespace: Boolean = true, + val allowUnicode: Boolean = false, + val maxTokenLength: Int = 1000 +) { + + init { + if (maxTokenLength <= 0) { + throw LexerException.maxTokenLengthInvalid(maxTokenLength) + } + + if (startTime <= 0) { + throw LexerException.startTimeInvalid(startTime) + } + + } + + companion object { + /** + * ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ๋ถ„์„ํ•  ์ž…๋ ฅ ํ…์ŠคํŠธ + * @param startTime ๋ถ„์„ ์‹œ์ž‘ ์‹œ๊ฐ„ (๊ธฐ๋ณธ๊ฐ’: ํ˜„์žฌ ์‹œ๊ฐ„) + * @return ๊ธฐ๋ณธ LexingContext + */ + fun of(input: String, startTime: Long = System.currentTimeMillis()): LexingContext = LexingContext( + input = input, + currentPosition = Position.START, + startTime = startTime + ) + + /** + * ์—„๊ฒฉ ๋ชจ๋“œ ์„ค์ •์œผ๋กœ ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ๋ถ„์„ํ•  ์ž…๋ ฅ ํ…์ŠคํŠธ + * @param strictMode ์—„๊ฒฉ ๋ชจ๋“œ ์—ฌ๋ถ€ + * @return ์„ค์ •๋œ LexingContext + */ + fun withStrictMode(input: String, strictMode: Boolean): LexingContext = + of(input).copy(strictMode = strictMode) + + /** + * ์œ ๋‹ˆ์ฝ”๋“œ ํ—ˆ์šฉ ์„ค์ •์œผ๋กœ ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ๋ถ„์„ํ•  ์ž…๋ ฅ ํ…์ŠคํŠธ + * @param allowUnicode ์œ ๋‹ˆ์ฝ”๋“œ ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @return ์„ค์ •๋œ LexingContext + */ + fun withUnicode(input: String, allowUnicode: Boolean): LexingContext = + of(input).copy(allowUnicode = allowUnicode) + + /** + * ์™„์ „ํ•œ ์˜ต์…˜์œผ๋กœ ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ๋ถ„์„ํ•  ์ž…๋ ฅ ํ…์ŠคํŠธ + * @param strictMode ์—„๊ฒฉ ๋ชจ๋“œ ์—ฌ๋ถ€ + * @param skipWhitespace ๊ณต๋ฐฑ ์Šคํ‚ต ์—ฌ๋ถ€ + * @param allowUnicode ์œ ๋‹ˆ์ฝ”๋“œ ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @param maxTokenLength ์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด + * @param startTime ๋ถ„์„ ์‹œ์ž‘ ์‹œ๊ฐ„ (๊ธฐ๋ณธ๊ฐ’: ํ˜„์žฌ ์‹œ๊ฐ„) + * @return ์„ค์ •๋œ LexingContext + */ + fun create( + input: String, + strictMode: Boolean = true, + skipWhitespace: Boolean = true, + allowUnicode: Boolean = false, + maxTokenLength: Int = 1000, + startTime: Long = System.currentTimeMillis() + ): LexingContext = LexingContext( + input = input, + currentPosition = Position.START, + startTime = startTime, + strictMode = strictMode, + skipWhitespace = skipWhitespace, + allowUnicode = allowUnicode, + maxTokenLength = maxTokenLength + ) + } + + /** + * ํ˜„์žฌ ์œ„์น˜๊ฐ€ ์ž…๋ ฅ ๋์— ๋„๋‹ฌํ–ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋์— ๋„๋‹ฌํ–ˆ์œผ๋ฉด true + */ + fun isAtEnd(): Boolean = currentPosition.index >= input.length + + /** + * ๋” ์ฝ์„ ๋ฌธ์ž๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฝ์„ ๋ฌธ์ž๊ฐ€ ์žˆ์œผ๋ฉด true + */ + fun hasNext(): Boolean = !isAtEnd() + + /** + * ํ˜„์žฌ ์œ„์น˜์˜ ๋ฌธ์ž (์บ์‹œ๋จ) + */ + val currentChar: Char? by lazy { + if (isAtEnd()) null else input[currentPosition.index] + } + + + /** + * ๋‹ค์Œ ์œ„์น˜์˜ ๋ฌธ์ž๋ฅผ ๋ฏธ๋ฆฌ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param offset ํ˜„์žฌ ์œ„์น˜์—์„œ์˜ ์˜คํ”„์…‹ (๊ธฐ๋ณธ๊ฐ’: 1) + * @return ํ•ด๋‹น ์œ„์น˜์˜ ๋ฌธ์ž ๋˜๋Š” null + */ + fun peekChar(offset: Int = 1): Char? { + val index = currentPosition.index + offset + return if (index >= input.length) null else input[index] + } + + /** + * ํ˜„์žฌ ์œ„์น˜์—์„œ ์ง€์ •๋œ ๊ธธ์ด๋งŒํผ์˜ ๋ถ€๋ถ„ ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param length ์ถ”์ถœํ•  ๊ธธ์ด + * @return ๋ถ€๋ถ„ ๋ฌธ์ž์—ด + */ + fun peek(length: Int): String { + val startIndex = currentPosition.index + val endIndex = minOf(startIndex + length, input.length) + return input.substring(startIndex, endIndex) + } + + /** + * ์œ„์น˜๋ฅผ ์•ž์œผ๋กœ ์ด๋™์‹œํ‚จ ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param steps ์ด๋™ํ•  ๋ฌธ์ž ์ˆ˜ (๊ธฐ๋ณธ๊ฐ’: 1) + * @return ์ด๋™๋œ LexingContext + */ + fun advance(steps: Int = 1): LexingContext { + if (steps < 0) { + throw LexerException.stepsNegative(steps) + } + + var index = currentPosition.index + var line = currentPosition.line + var column = currentPosition.column + + val maxIndex = input.length + var moved = 0 + + while (moved < steps && index < maxIndex) { + val currentChar = input[index++] + moved++ + if (currentChar == '\n') { + line++ + column = 1 + } else { + column++ + } + } + + return copy(currentPosition = Position(index, line, column)) + } + + /** + * ํŠน์ • ์œ„์น˜๋กœ ์ด๋™ํ•œ ์ƒˆ๋กœ์šด ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param position ์ด๋™ํ•  ์œ„์น˜ + * @return ์ด๋™๋œ LexingContext + */ + fun moveTo(position: Position): LexingContext = copy(currentPosition = position) + + /** + * ํ˜„์žฌ ์œ„์น˜์—์„œ ์ง€์ •๋œ ๋ ์œ„์น˜๊นŒ์ง€์˜ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param endPosition ๋ ์œ„์น˜ + * @return ์ถ”์ถœ๋œ ํ…์ŠคํŠธ + */ + fun extractText(endPosition: Position): String { + val startIndex = currentPosition.index + val endIndex = minOf(endPosition.index, input.length) + return if (startIndex <= endIndex) { + input.substring(startIndex, endIndex) + } else { + "" + } + } + + /** + * ํ˜„์žฌ ์œ„์น˜์—์„œ ์ง€์ •๋œ ๊ธธ์ด๋งŒํผ์˜ ํ…์ŠคํŠธ๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param length ์ถ”์ถœํ•  ๊ธธ์ด + * @return ์ถ”์ถœ๋œ ํ…์ŠคํŠธ + */ + fun extractText(length: Int): String { + val startIndex = currentPosition.index + val endIndex = minOf(startIndex + length, input.length) + return input.substring(startIndex, endIndex) + } + + /** + * ํ˜„์žฌ ์œ„์น˜๊ฐ€ ํŠน์ • ๋ฌธ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param char ํ™•์ธํ•  ๋ฌธ์ž + * @return ์ผ์น˜ํ•˜๋ฉด true + */ + fun isCurrentChar(char: Char): Boolean = currentChar == char + + /** + * ํ˜„์žฌ ์œ„์น˜๊ฐ€ ํŠน์ • ๋ฌธ์ž๋“ค ์ค‘ ํ•˜๋‚˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param chars ํ™•์ธํ•  ๋ฌธ์ž๋“ค + * @return ์ผ์น˜ํ•˜๋Š” ๋ฌธ์ž๊ฐ€ ์žˆ์œผ๋ฉด true + */ + fun isCurrentCharIn(chars: Set): Boolean = + currentChar?.let { it in chars } ?: false + + /** + * ํ˜„์žฌ ์œ„์น˜๊ฐ€ ์ˆซ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆซ์ž์ด๋ฉด true + */ + fun isCurrentDigit(): Boolean = currentChar?.isDigit() ?: false + + /** + * ํ˜„์žฌ ์œ„์น˜๊ฐ€ ๋ฌธ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฌธ์ž์ด๋ฉด true + */ + fun isCurrentLetter(): Boolean = currentChar?.isLetter() ?: false + + /** + * ํ˜„์žฌ ์œ„์น˜๊ฐ€ ๊ณต๋ฐฑ ๋ฌธ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณต๋ฐฑ ๋ฌธ์ž์ด๋ฉด true + */ + fun isCurrentWhitespace(): Boolean = currentChar?.isWhitespace() ?: false + + /** + * ๋‹ค์Œ N๊ฐœ ๋ฌธ์ž๊ฐ€ ํŠน์ • ๋ฌธ์ž์—ด๊ณผ ์ผ์น˜ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ํ™•์ธํ•  ๋ฌธ์ž์—ด + * @return ์ผ์น˜ํ•˜๋ฉด true + */ + fun matchesNext(text: String): Boolean = peek(text.length) == text + + /** + * ๋ถ„์„ ๊ฒฝ๊ณผ ์‹œ๊ฐ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฒฝ๊ณผ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun getElapsedTime(): Long = System.currentTimeMillis() - startTime + + /** + * ๋‚จ์€ ์ž…๋ ฅ ํ…์ŠคํŠธ ๊ธธ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‚จ์€ ํ…์ŠคํŠธ ๊ธธ์ด + */ + fun getRemainingLength(): Int = maxOf(0, input.length - currentPosition.index) + + /** + * ์ง„ํ–‰๋ฅ ์„ ๋ฐฑ๋ถ„์œจ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง„ํ–‰๋ฅ  (0.0 ~ 100.0) + */ + fun getProgress(): Double = + if (input.isEmpty()) 100.0 + else (currentPosition.index.toDouble() / input.length) * 100.0 + + /** + * ์ปจํ…์ŠคํŠธ์˜ ์ƒํƒœ ์ •๋ณด๋ฅผ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒํƒœ ์ •๋ณด ๋งต + */ + fun getState(): Map = mapOf( + "inputLength" to input.length, + "currentIndex" to currentPosition.index, + "currentLine" to currentPosition.line, + "currentColumn" to currentPosition.column, + "elapsedTime" to getElapsedTime(), + "progress" to getProgress(), + "strictMode" to strictMode, + "skipWhitespace" to skipWhitespace, + "allowUnicode" to allowUnicode, + "maxTokenLength" to maxTokenLength + ) + + /** + * ์ปจํ…์ŠคํŠธ ์ •๋ณด๋ฅผ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ปจํ…์ŠคํŠธ ์ •๋ณด ๋ฌธ์ž์—ด + */ + override fun toString(): String = buildString { + append("LexingContext(") + append("pos=${currentPosition.toShortString()}, ") + append("input=${input.length} chars, ") + append("progress=${String.format("%.1f", getProgress())}%") + append(")") + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/values/LexingResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/values/LexingResult.kt new file mode 100644 index 00000000..ded4bd1c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/lexer/values/LexingResult.kt @@ -0,0 +1,248 @@ +package hs.kr.entrydsm.domain.lexer.values + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.lexer.exceptions.LexerException + +/** + * ์–ดํœ˜ ๋ถ„์„(Lexing) ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * Lexer์˜ ์ž…๋ ฅ ํ…์ŠคํŠธ ๋ถ„์„ ๊ฒฐ๊ณผ๋กœ, ์ƒ์„ฑ๋œ ํ† ํฐ ๋ชฉ๋ก๊ณผ ๋ถ„์„ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•œ + * ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€์™€ ๊ด€๋ จ ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ์ œ๊ณตํ•˜์—ฌ + * Parser์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์™„์ „ํ•œ ํ† ํฐ ์ŠคํŠธ๋ฆผ์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @property tokens ์ƒ์„ฑ๋œ ํ† ํฐ ๋ชฉ๋ก + * @property isSuccess ๋ถ„์„ ์„ฑ๊ณต ์—ฌ๋ถ€ + * @property error ๋ถ„์„ ์‹คํŒจ ์‹œ์˜ ์˜ค๋ฅ˜ ์ •๋ณด + * @property duration ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + * @property inputLength ์ž…๋ ฅ ํ…์ŠคํŠธ ๊ธธ์ด + * @property tokenCount ์ƒ์„ฑ๋œ ํ† ํฐ ๊ฐœ์ˆ˜ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +data class LexingResult( + val tokens: List, + val isSuccess: Boolean = true, + val error: LexerException? = null, + val duration: Long = 0L, + val inputLength: Int = 0, + val tokenCount: Int = tokens.size +) { + + init { + if (!(isSuccess || error != null)) { + throw LexerException.invalidLexingResultErrorState(isSuccess, error) + } + + if (duration < 0) { + throw LexerException.negativeAnalysisDuration(duration) + } + + if (inputLength < 0) { + throw LexerException.negativeInputLength(inputLength) + } + + if (tokenCount < 0) { + throw LexerException.negativeTokenCount(tokenCount) + } + } + + companion object { + /** + * ์„ฑ๊ณต์ ์ธ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ์ƒ์„ฑ๋œ ํ† ํฐ ๋ชฉ๋ก + * @param duration ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„ + * @param inputLength ์ž…๋ ฅ ํ…์ŠคํŠธ ๊ธธ์ด + * @return ์„ฑ๊ณต LexingResult + */ + fun success( + tokens: List, + duration: Long = 0L, + inputLength: Int = 0 + ): LexingResult = LexingResult( + tokens = tokens, + isSuccess = true, + error = null, + duration = duration, + inputLength = inputLength, + tokenCount = tokens.size + ) + + /** + * ์‹คํŒจํ•œ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param error ๋ถ„์„ ์˜ค๋ฅ˜ ์ •๋ณด + * @param partialTokens ๋ถ€๋ถ„์ ์œผ๋กœ ์ƒ์„ฑ๋œ ํ† ํฐ๋“ค + * @param duration ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„ + * @param inputLength ์ž…๋ ฅ ํ…์ŠคํŠธ ๊ธธ์ด + * @return ์‹คํŒจ LexingResult + */ + fun failure( + error: LexerException, + partialTokens: List = emptyList(), + duration: Long = 0L, + inputLength: Int = 0 + ): LexingResult = LexingResult( + tokens = partialTokens, + isSuccess = false, + error = error, + duration = duration, + inputLength = inputLength, + tokenCount = partialTokens.size + ) + + /** + * ๋นˆ ์„ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param inputLength ์ž…๋ ฅ ํ…์ŠคํŠธ ๊ธธ์ด + * @return ๋นˆ ์„ฑ๊ณต LexingResult + */ + fun empty(inputLength: Int = 0): LexingResult = success( + tokens = emptyList(), + inputLength = inputLength + ) + + private const val UNKNOWN_ERROR = "Unknown error" + } + + /** + * ๋ถ„์„ ์‹คํŒจ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹คํŒจํ–ˆ์œผ๋ฉด true + */ + fun isFailure(): Boolean = !isSuccess + + /** + * ํ† ํฐ์ด ํ•˜๋‚˜๋„ ์ƒ์„ฑ๋˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ† ํฐ์ด ์—†์œผ๋ฉด true + */ + fun isEmpty(): Boolean = tokens.isEmpty() + + /** + * ํ† ํฐ์ด ํ•˜๋‚˜ ์ด์ƒ ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ† ํฐ์ด ์žˆ์œผ๋ฉด true + */ + fun isNotEmpty(): Boolean = tokens.isNotEmpty() + + /** + * ํŠน์ • ํƒ€์ž…์˜ ํ† ํฐ๋“ค์„ ํ•„ํ„ฐ๋งํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ์ฐพ์„ ํ† ํฐ ํƒ€์ž… + * @return ํ•ด๋‹น ํƒ€์ž…์˜ ํ† ํฐ ๋ชฉ๋ก + */ + fun filterByType(type: TokenType): List = + tokens.filter { it.type == type } + + /** + * ์ฒซ ๋ฒˆ์งธ ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฒซ ๋ฒˆ์งธ ํ† ํฐ ๋˜๋Š” null + */ + fun firstToken(): Token? = tokens.firstOrNull() + + /** + * ๋งˆ์ง€๋ง‰ ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋งˆ์ง€๋ง‰ ํ† ํฐ ๋˜๋Š” null + */ + fun lastToken(): Token? = tokens.lastOrNull() + + /** + * ํŠน์ • ์ธ๋ฑ์Šค์˜ ํ† ํฐ์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ํ† ํฐ ์ธ๋ฑ์Šค + * @return ํ•ด๋‹น ์ธ๋ฑ์Šค์˜ ํ† ํฐ ๋˜๋Š” null + */ + fun getTokenAt(index: Int): Token? = tokens.getOrNull(index) + + /** + * ์—ฐ์‚ฐ์ž ํ† ํฐ๋“ค๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฐ์‚ฐ์ž ํ† ํฐ ๋ชฉ๋ก + */ + fun getOperatorTokens(): List = + tokens.filter { it.type.isOperator } + + /** + * ๋ฆฌํ„ฐ๋Ÿด ํ† ํฐ๋“ค๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฆฌํ„ฐ๋Ÿด ํ† ํฐ ๋ชฉ๋ก + */ + fun getLiteralTokens(): List = + tokens.filter { it.type.isLiteral } + + /** + * ํ‚ค์›Œ๋“œ ํ† ํฐ๋“ค๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ‚ค์›Œ๋“œ ํ† ํฐ ๋ชฉ๋ก + */ + fun getKeywordTokens(): List = + tokens.filter { it.type.isKeyword } + + /** + * ๋ถ„์„ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map { + var operatorCount = 0 + var literalCount = 0 + var keywordCount = 0 + + for (token in tokens) { + if (token.type.isOperator) operatorCount++ + if (token.type.isLiteral) literalCount++ + if (token.type.isKeyword) keywordCount++ + } + + return buildMap { + put("success", isSuccess) + put("tokenCount", tokenCount) + put("inputLength", inputLength) + put("duration", duration) + put("operatorCount", operatorCount) + put("literalCount", literalCount) + put("keywordCount", keywordCount) + if (!isSuccess) put("errorMessage", error?.message ?: UNKNOWN_ERROR) + } + } + + /** + * ํ† ํฐ ๋ชฉ๋ก์„ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ† ํฐ ๋ชฉ๋ก ๋ฌธ์ž์—ด + */ + fun tokensToString(): String = tokens.joinToString(" ") { it.toString() } + + /** + * ๋ถ„์„ ๊ฒฐ๊ณผ์˜ ์š”์•ฝ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์š”์•ฝ ์ •๋ณด ๋ฌธ์ž์—ด + */ + fun getSummary(): String = buildString { + append("LexingResult(") + append("success=$isSuccess, ") + append("tokens=$tokenCount, ") + append("duration=${duration}ms") + if (inputLength > 0) { + append(", input=$inputLength chars") + } + if (error != null) { + append(", error=${error.message}") + } + append(")") + } + + /** + * ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ธ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ์ •๋ณด ๋ฌธ์ž์—ด + */ + override fun toString(): String = getSummary() +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/AutomaticLRParserGenerator.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/AutomaticLRParserGenerator.kt new file mode 100644 index 00000000..e73ce3a2 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/AutomaticLRParserGenerator.kt @@ -0,0 +1,839 @@ +package hs.kr.entrydsm.domain.parser.aggregates + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.calculator.interfaces.CalculatorContract +import hs.kr.entrydsm.domain.calculator.services.ValidationService +import hs.kr.entrydsm.domain.evaluator.interfaces.EvaluatorContract +import hs.kr.entrydsm.domain.evaluator.values.EvaluationResult +import hs.kr.entrydsm.domain.expresser.interfaces.ExpresserContract +import hs.kr.entrydsm.domain.expresser.values.FormattedExpression +import hs.kr.entrydsm.domain.lexer.contract.LexerContract +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.parser.aggregates.AutomaticLRParserGenerator.AutomaticLRPGConsts.COMPLETE_EXPRESSION_PROCESSING +import hs.kr.entrydsm.domain.parser.entities.ParsingState +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.factories.LRItemFactory +import hs.kr.entrydsm.domain.parser.factories.ParsingStateFactory +import hs.kr.entrydsm.domain.parser.interfaces.GrammarProvider +import hs.kr.entrydsm.domain.parser.policies.LALRMergingPolicy +import hs.kr.entrydsm.domain.parser.services.FirstFollowCalculatorService +import hs.kr.entrydsm.domain.parser.services.LRParserTableService +import hs.kr.entrydsm.domain.parser.services.RealLRParserService +import hs.kr.entrydsm.domain.parser.values.Grammar +import hs.kr.entrydsm.domain.parser.values.ParsingResult +import hs.kr.entrydsm.domain.parser.values.ParsingTable +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import hs.kr.entrydsm.global.values.Result +import java.util.concurrent.CompletableFuture +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors + +/** + * ์ž๋™ LR(1) ํŒŒ์„œ ์ƒ์„ฑ๊ธฐ ์ง‘ํ•ฉ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค. + * + * ์™„์ „ํ•œ LR(1) ์ƒํƒœ ์ž๋™ ์ƒ์„ฑ, LALR ์ตœ์ ํ™”, ์ˆ˜์‹ ์—ฐ์‚ฐ ์‹คํ–‰ ๋ฐ ๊ฒ€์ฆ์„ + * ๋ชจ๋“  ๊ธฐ์กด ํ•จ์ˆ˜๋“ค์„ ๊ทนํ•œ์œผ๋กœ ํ™œ์šฉํ•˜์—ฌ ํ†ตํ•ฉ ์ฒ˜๋ฆฌํ•˜๋Š” ์‹œ์Šคํ…œ์ž…๋‹ˆ๋‹ค. + * + * ์ฃผ์š” ๊ธฐ๋Šฅ: + * - ์ž๋™ LR(1) ์ƒํƒœ ์ƒ์„ฑ ๋ฐ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ตฌ์ถ• + * - LALR ์ƒํƒœ ๋ณ‘ํ•ฉ ์ตœ์ ํ™” + * - ์ˆ˜์‹ ์—ฐ์‚ฐ์˜ ์ „์ฒด ํŒŒ์ดํ”„๋ผ์ธ (ํ† ํฐํ™” โ†’ ํŒŒ์‹ฑ โ†’ ํ‰๊ฐ€ โ†’ ๊ฒ€์ฆ) + * - ์‹ค์‹œ๊ฐ„ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์ตœ์ ํ™” + * - ๋ฉ€ํ‹ฐ์Šค๋ ˆ๋“œ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.30 + */ +@Aggregate(context = "parser") +class AutomaticLRParserGenerator( + private val grammarProvider: GrammarProvider, + private val lexerContract: LexerContract, + private val calculatorContract: CalculatorContract, + private val evaluatorContract: EvaluatorContract, + private val expresserContract: ExpresserContract, + private val validationService: ValidationService, + private val firstFollowCalculatorService: FirstFollowCalculatorService, + private val lrParserTableService: LRParserTableService, + private val lrItemFactory: LRItemFactory, + private val parsingStateFactory: ParsingStateFactory, + private val lalrMergingPolicy: LALRMergingPolicy +) { + + // ์Šค๋ ˆ๋“œ ํ’€ ๋ฐ ์บ์‹œ + private val executorService = Executors.newFixedThreadPool(AutomaticLRPGConsts.THREAD_POOL_SIZE) + private val parsingTableCache = ConcurrentHashMap() + private val expressionCache = ConcurrentHashMap() + private val performanceMetrics = ConcurrentHashMap() + + // ํŒŒ์„œ ์ธ์Šคํ„ด์Šค๋“ค + private var currentParsingTable: ParsingTable? = null + private var currentRealParserService: RealLRParserService? = null + + // ์„ฑ๋Šฅ ํ†ต๊ณ„ + private var totalEvaluations = 0L + private var successfulEvaluations = 0L + private var failedEvaluations = 0L + private var totalProcessingTime = 0L + + init { + // ๊ธฐ๋ณธ ๋ฌธ๋ฒ•์œผ๋กœ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ์ดˆ๊ธฐํ™” + initializeDefaultParsingTable() + } + + /** + * ์™„์ „ํ•œ ์ˆ˜์‹ ์ฒ˜๋ฆฌ ํŒŒ์ดํ”„๋ผ์ธ์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * ๋ชจ๋“  ๊ธฐ์กด ํ•จ์ˆ˜๋“ค์„ ๊ทนํ•œ์œผ๋กœ ํ™œ์šฉํ•˜์—ฌ ํ†ตํ•ฉ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param expression ์ฒ˜๋ฆฌํ•  ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @param variables ๋ณ€์ˆ˜ ๋งต (์„ ํƒ์‚ฌํ•ญ) + * @param enableOptimization LALR ์ตœ์ ํ™” ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @param enableValidation ๊ฒ€์ฆ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @return ์™„์ „ํ•œ ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ + */ + fun processExpressionComplete( + expression: String, + variables: Map = emptyMap(), + enableOptimization: Boolean = true, + enableValidation: Boolean = true + ): CompletableExpressionResult { + val startTime = System.currentTimeMillis() + val operationId = generateOperationId() + + return try { + recordPerformanceStart(operationId, COMPLETE_EXPRESSION_PROCESSING) + + // 1. ์บ์‹œ ํ™•์ธ + val cacheKey = generateCacheKey(expression, variables, enableOptimization, enableValidation) + expressionCache[cacheKey]?.let { cached -> + recordPerformanceEnd(operationId, System.currentTimeMillis() - startTime, true) + return cached.toCompletableResult(true) + } + + // 2. ์ž…๋ ฅ ๊ฒ€์ฆ (ValidationService ๊ทนํ•œ ํ™œ์šฉ) + val validationResult = if (enableValidation) { + performComprehensiveValidation(expression, variables) + } else { + ValidationResult.success() + } + + if (!validationResult.isValid) { + val result = CompletableExpressionResult.failure( + error = ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Common.VALIDATION_FAILED, + message = "์ž…๋ ฅ ๊ฒ€์ฆ ์‹คํŒจ: ${validationResult.errors.joinToString(", ")}" + ), + processingTime = System.currentTimeMillis() - startTime, + operationId = operationId + ) + failedEvaluations++ + return result + } + + // 3. ํ† ํฐํ™” (LexerContract ํ™œ์šฉ) + val lexingResult = lexerContract.tokenize(expression) + if (!lexingResult.isSuccess) { + val result = CompletableExpressionResult.failure( + error = ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Lexer.TOKENIZATION_FAILED, + message = "ํ† ํฐํ™” ์‹คํŒจ: ${lexingResult.error?.message}" + ), + processingTime = System.currentTimeMillis() - startTime, + operationId = operationId + ) + failedEvaluations++ + return result + } + + // 4. LR(1) ์ƒํƒœ ์ž๋™ ์ƒ์„ฑ ๋ฐ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ตฌ์ถ• + val grammar = Grammar + val parsingTable = if (enableOptimization) { + buildOptimizedLALRParsingTable(grammar) + } else { + buildStandardLR1ParsingTable(grammar) + } + + // 5. ํŒŒ์‹ฑ (RealLRParserService ํ™œ์šฉ) + val realParserService = createRealParserService(parsingTable) + val parsingResult = realParserService.parse(lexingResult.tokens) + + if (!parsingResult.isSuccess) { + val result = CompletableExpressionResult.failure( + error = parsingResult.error ?: ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSING_FAILED, + message = "ํŒŒ์‹ฑ ์‹คํŒจ" + ), + processingTime = System.currentTimeMillis() - startTime, + operationId = operationId + ) + failedEvaluations++ + return result + } + + // 6. AST ์ตœ์ ํ™” (๊ธฐ์กด TreeOptimizer ํ™œ์šฉ) + val optimizedAST = if (enableOptimization) { + optimizeAST(parsingResult.ast!!) + } else { + parsingResult.ast!! + } + + // 7. ํ‘œํ˜„์‹ ํ‰๊ฐ€ (EvaluatorContract ๊ทนํ•œ ํ™œ์šฉ) + val evaluationResult = evaluatorContract.evaluate(optimizedAST, variables) + + // 8. ๊ฒฐ๊ณผ ํฌ๋งคํŒ… (ExpresserContract ํ™œ์šฉ) + val formattedResult = expresserContract.format(optimizedAST) + + // 9. ์ตœ์ข… ๊ฒ€์ฆ (CalculatorContract ํ™œ์šฉ) + val finalValidation = if (enableValidation) { + performFinalValidation(evaluationResult, formattedResult, optimizedAST) + } else { + FinalValidationResult.success() + } + + val processingTime = System.currentTimeMillis() - startTime + + // 10. ๊ฒฐ๊ณผ ๊ตฌ์„ฑ + val result = CompletableExpressionResult.success( + originalExpression = expression, + tokens = lexingResult.tokens, + ast = optimizedAST, + evaluationResult = evaluationResult, + formattedResult = formattedResult, + validationResult = validationResult, + finalValidation = finalValidation, + parsingMetadata = createExtendedParsingMetadata(parsingResult, realParserService), + optimizationApplied = enableOptimization, + validationApplied = enableValidation, + processingTime = processingTime, + operationId = operationId, + performanceMetrics = createPerformanceMetrics(startTime, processingTime) + ) + + // 11. ์บ์‹œ ์ €์žฅ + cacheExpressionResult(cacheKey, result) + + // 12. ์„ฑ๋Šฅ ๊ธฐ๋ก + recordPerformanceEnd(operationId, processingTime, true) + successfulEvaluations++ + totalProcessingTime += processingTime + + result + + } catch (e: Exception) { + val processingTime = System.currentTimeMillis() - startTime + val result = CompletableExpressionResult.failure( + error = e, + processingTime = processingTime, + operationId = operationId + ) + + recordPerformanceEnd(operationId, processingTime, false) + failedEvaluations++ + totalProcessingTime += processingTime + + result + } finally { + totalEvaluations++ + } + } + + /** + * ์ž๋™ LR(1) ์ƒํƒœ ์ƒ์„ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * LRParserTableService์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. + */ + fun generateLR1States(grammar: Grammar): Map { + val productions = grammar.productions + grammar.augmentedProduction + val firstSets = firstFollowCalculatorService.calculateFirstSets( + productions, grammar.terminals, grammar.nonTerminals + ) + + return lrParserTableService.buildLR1States( + productions, grammar.startSymbol, firstSets + ) + } + + /** + * LALR ์ตœ์ ํ™”๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * LALRMergingPolicy์˜ ๋ชจ๋“  ๊ธฐ๋Šฅ์„ ๊ทนํ•œ ํ™œ์šฉํ•ฉ๋‹ˆ๋‹ค. + */ + fun applyLALROptimization(lr1States: Map): Map { + // 1. ์—„๊ฒฉํ•œ ๋ณ‘ํ•ฉ ๋ชจ๋“œ ์„ค์ • + lalrMergingPolicy.setStrictMerging(true) + lalrMergingPolicy.setAllowConflictMerging(false) + + // 2. LALR ์••์ถ• ์ˆ˜ํ–‰ + val compressedStates = lalrMergingPolicy.compressStatesLALR(lr1States) + + // 3. ๋ณ‘ํ•ฉ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + val isValid = lalrMergingPolicy.validateLALRMerging(lr1States, compressedStates) + + if (!isValid) { + throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.AST.TREE_OPTIMIZATION_FAILED, + message = "LALR ๋ณ‘ํ•ฉ ๊ฒ€์ฆ ์‹คํŒจ" + ) + } + + return compressedStates + } + + /** + * ๋น„๋™๊ธฐ๋กœ ์—ฌ๋Ÿฌ ์ˆ˜์‹์„ ๋ณ‘๋ ฌ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + fun processExpressionsAsync( + expressions: List, + variables: Map = emptyMap(), + enableOptimization: Boolean = true, + enableValidation: Boolean = true + ): CompletableFuture> { + return CompletableFuture.supplyAsync({ + val futures = expressions.map { expression -> + CompletableFuture.supplyAsync({ + processExpressionComplete(expression, variables, enableOptimization, enableValidation) + }, executorService) + } + + CompletableFuture.allOf(*futures.toTypedArray()).join() + futures.map { it.get() } + }, executorService) + } + + /** + * ์‹ค์‹œ๊ฐ„ ์„ฑ๋Šฅ ๋ชจ๋‹ˆํ„ฐ๋ง ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getRealtimePerformanceMetrics(): Map = mapOf( + AutomaticLRPGConsts.M_TOTAL_EVAL to totalEvaluations, + AutomaticLRPGConsts.M_SUCCESSFUL_EVAL to successfulEvaluations, + AutomaticLRPGConsts.M_FAILED_EVAL to failedEvaluations, + AutomaticLRPGConsts.M_SUCCESS_RATE to if (totalEvaluations > 0) successfulEvaluations.toDouble() / totalEvaluations else 0.0, + AutomaticLRPGConsts.M_AVG_PROC_TIME to if (totalEvaluations > 0) totalProcessingTime.toDouble() / totalEvaluations else 0.0, + AutomaticLRPGConsts.M_CACHE_STATS to mapOf( + AutomaticLRPGConsts.C_PARSING_TABLE_CACHE_SIZE to parsingTableCache.size, + AutomaticLRPGConsts.C_EXPRESSION_CACHE_SIZE to expressionCache.size, + AutomaticLRPGConsts.C_CACHE_HIT_RATE to calculateCacheHitRate() + ), + AutomaticLRPGConsts.M_CURRENT_LOAD to performanceMetrics.size, + AutomaticLRPGConsts.M_THREAD_POOL_STATUS to mapOf( + AutomaticLRPGConsts.T_ACTIVE_THREADS to AutomaticLRPGConsts.THREAD_POOL_SIZE, + AutomaticLRPGConsts.T_PENDING_TASKS to 0 // executorService ์ƒํƒœ ์ •๋ณด + ), + AutomaticLRPGConsts.M_MEMORY_USAGE to getMemoryUsageInfo(), + AutomaticLRPGConsts.M_RECENT_PERF to getRecentPerformanceData() + ) + + /** + * ์‹œ์Šคํ…œ ์ง„๋‹จ ๋ฐ ์ตœ์ ํ™” ์ œ์•ˆ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun diagnoseAndOptimize(): SystemDiagnosisResult { + val metrics = getRealtimePerformanceMetrics() + val recommendations = mutableListOf() + val issues = mutableListOf() + + // ์„ฑ๋Šฅ ๋ถ„์„ + val avgProcessingTime = metrics[AutomaticLRPGConsts.M_AVG_PROC_TIME] as Double + if (avgProcessingTime > AutomaticLRPGConsts.PERFORMANCE_THRESHOLD_MS) { + issues.add(AutomaticLRPGConsts.ISSUE_AVG_TIME.format(avgProcessingTime, AutomaticLRPGConsts.PERFORMANCE_THRESHOLD_MS)) + recommendations.add(AutomaticLRPGConsts.RECO_ENABLE_OPT_OR_CACHE) + } + + // ์บ์‹œ ๋ถ„์„ + val cacheHitRate = calculateCacheHitRate() + if (cacheHitRate < 0.8) { + issues.add(AutomaticLRPGConsts.ISSUE_CACHE_HIT.format(cacheHitRate * 100)) + recommendations.add(AutomaticLRPGConsts.RECO_INCREASE_OR_TUNE_CACHE) + } + + // ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๋ถ„์„ + val memoryUsage = getMemoryUsageInfo() + val usedMemoryPercentage = memoryUsage[AutomaticLRPGConsts.MEM_USED_PCT] as Double + if (usedMemoryPercentage > 85.0) { + issues.add(AutomaticLRPGConsts.ISSUE_MEM_USAGE.format(usedMemoryPercentage)) + recommendations.add(AutomaticLRPGConsts.RECO_GC_OR_SHRINK_CACHE) + } + + return SystemDiagnosisResult( + timestamp = System.currentTimeMillis(), + overallHealth = when { + issues.isEmpty() -> AutomaticLRPGConsts.HEALTH_HEALTHY + issues.size <= 2 -> AutomaticLRPGConsts.HEALTH_WARNING + else -> AutomaticLRPGConsts.HEALTH_CRITICAL + }, + issues = issues, + recommendations = recommendations, + metrics = metrics, + optimizationApplied = performAutomaticOptimization(issues) + ) + } + + // Private helper methods + + private fun initializeDefaultParsingTable() { + try { + val grammar = Grammar + currentParsingTable = buildStandardLR1ParsingTable(grammar) + currentRealParserService = createRealParserService(currentParsingTable!!) + } catch (e: Exception) { + // ๊ธฐ๋ณธ ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์‹คํŒจ ์‹œ ๋กœ๊น…๋งŒ ์ˆ˜ํ–‰ + println("๊ธฐ๋ณธ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ์ดˆ๊ธฐํ™” ์‹คํŒจ: ${e.message}") + } + } + + private fun buildStandardLR1ParsingTable(grammar: Grammar): ParsingTable { + val cacheKey = AutomaticLRPGConsts.CACHE_KEY_LR1_PREFIX.format(grammar.hashCode()) + parsingTableCache[cacheKey]?.let { return it } + + val table = lrParserTableService.buildParsingTable(grammar) + parsingTableCache[cacheKey] = table + return table + } + + private fun buildOptimizedLALRParsingTable(grammar: Grammar): ParsingTable { + val cacheKey = AutomaticLRPGConsts.CACHE_KEY_LALR_PREFIX.format(grammar.hashCode()) + parsingTableCache[cacheKey]?.let { return it } + + // 1. LR(1) ์ƒํƒœ ์ƒ์„ฑ + val lr1States = generateLR1States(grammar) + + // 2. LALR ์ตœ์ ํ™” ์ ์šฉ + val lalrStates = applyLALROptimization(lr1States) + + // 3. ์ตœ์ ํ™”๋œ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ตฌ์ถ• + val productions = grammar.productions + grammar.augmentedProduction + val table = lrParserTableService.constructParsingTable( + lalrStates, productions, grammar.terminals, grammar.nonTerminals + ) + + parsingTableCache[cacheKey] = table + return table + } + + private fun createRealParserService(parsingTable: ParsingTable): RealLRParserService { + return RealLRParserService(grammarProvider, parsingTable).apply { + setErrorRecoveryEnabled(true) + setDebuggingEnabled(false) + setMaxStackSize(10000) + } + } + + private fun performComprehensiveValidation( + expression: String, + variables: Map + ): ValidationResult { + val errors = mutableListOf() + + try { + // 1. ๊ธฐ๋ณธ ์ˆ˜์‹ ๊ฒ€์ฆ + validationService.validateFormula(expression) + + // 2. ๊ตฌ๋ฌธ ๊ฒ€์ฆ + validationService.validateSyntax(expression) + + // 3. ๋ณต์žก๋„ ๊ฒ€์ฆ + validationService.validateComplexity(expression) + + // 4. ๋ณ€์ˆ˜ ๊ฒ€์ฆ + if (variables.isNotEmpty()) { + validationService.validateVariableCount(variables) + variables.forEach { (name, value) -> + validationService.validateVariableValue(name, value) + } + } + + // 5. ๊ณ„์‚ฐ๊ธฐ ๋ ˆ๋ฒจ ๊ฒ€์ฆ + val isValid = calculatorContract.validateExpression(expression, variables) + if (!isValid) { + errors.add("๊ณ„์‚ฐ๊ธฐ ๋ ˆ๋ฒจ ๊ฒ€์ฆ ์‹คํŒจ") + } + + } catch (e: Exception) { + errors.add(e.message ?: "์•Œ ์ˆ˜ ์—†๋Š” ๊ฒ€์ฆ ์˜ค๋ฅ˜") + } + + return ValidationResult(errors.isEmpty(), errors) + } + + private fun optimizeAST(ast: ASTNode): ASTNode { + // TreeOptimizer ํ™œ์šฉ (๊ธฐ์กด ๊ตฌํ˜„๋œ ์ตœ์ ํ™” ๋กœ์ง) + return ast // ์‹ค์ œ๋กœ๋Š” TreeOptimizer.optimize(ast) ํ˜ธ์ถœ + } + + private fun performFinalValidation( + evaluationResult: EvaluationResult, + formattedResult: FormattedExpression, + ast: ASTNode + ): FinalValidationResult { + val issues = mutableListOf() + + // 1. ํ‰๊ฐ€ ๊ฒฐ๊ณผ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + if (!evaluationResult.isSuccess) { + issues.add("ํ‰๊ฐ€ ๊ฒฐ๊ณผ๊ฐ€ ์„ฑ๊ณต์ ์ด์ง€ ์•Š์Šต๋‹ˆ๋‹ค") + } + + // 2. AST ๊ตฌ์กฐ ๊ฒ€์ฆ + if (!ast.validate()) { + issues.add("AST ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") + } + + // 3. ํฌ๋งทํŒ… ๊ฒฐ๊ณผ ๊ฒ€์ฆ + if (formattedResult.expression.isBlank()) { + issues.add("ํฌ๋งทํŒ… ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค") + } + + return FinalValidationResult(issues.isEmpty(), issues) + } + + private fun createExtendedParsingMetadata( + parsingResult: ParsingResult, + realParserService: RealLRParserService + ): Map { + val baseMetadata = parsingResult.metadata + val serviceStats = realParserService.getStatistics() + val serviceConfig = realParserService.getConfiguration() + + return baseMetadata + mapOf( + AutomaticLRPGConsts.X_PARSER_SERVICE_STATS to serviceStats, + AutomaticLRPGConsts.X_PARSER_SERVICE_CONFIG to serviceConfig, + AutomaticLRPGConsts.X_PARSING_TRACE to realParserService.getParsingTrace().takeLast(10) // ์ตœ๊ทผ 10๊ฐœ๋งŒ + ) + } + + private fun createPerformanceMetrics(startTime: Long, totalTime: Long): Map = mapOf( + AutomaticLRPGConsts.PM_START_TIME to startTime, + AutomaticLRPGConsts.PM_TOTAL_TIME to totalTime, + AutomaticLRPGConsts.PM_THROUGHPUT to if (totalTime > 0) 1000.0 / totalTime else 0.0, + AutomaticLRPGConsts.PM_EFFICIENCY to calculateEfficiency(totalTime), + AutomaticLRPGConsts.PM_RESOURCE_UTIL to calculateResourceUtilization() + ) + + private fun generateOperationId(): String = + AutomaticLRPGConsts.OP_ID_TEMPLATE.format(System.currentTimeMillis(), Thread.currentThread().id) + + private fun generateCacheKey( + expression: String, + variables: Map, + enableOptimization: Boolean, + enableValidation: Boolean + ): String = "${expression.hashCode()}_${variables.hashCode()}_${enableOptimization}_${enableValidation}" + + private fun cacheExpressionResult(key: String, result: CompletableExpressionResult) { + if (expressionCache.size >= AutomaticLRPGConsts.MAX_EXPRESSION_CACHE_SIZE) { + // LRU ์บ์‹œ ์ •์ฑ…: ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ํ•ญ๋ชฉ ์ œ๊ฑฐ + val oldestKey = expressionCache.keys.first() + expressionCache.remove(oldestKey) + } + + expressionCache[key] = ExpressionEvaluationResult( + result = result, + timestamp = System.currentTimeMillis(), + accessCount = 1 + ) + } + + private fun recordPerformanceStart(operationId: String, operationType: String) { + performanceMetrics[operationId] = PerformanceMetric( + operationId = operationId, + operationType = operationType, + startTime = System.currentTimeMillis(), + status = AutomaticLRPGConsts.STATUS_RUNNING + ) + } + + private fun recordPerformanceEnd(operationId: String, duration: Long, success: Boolean) { + performanceMetrics[operationId]?.let { metric -> + performanceMetrics[operationId] = metric.copy( + endTime = System.currentTimeMillis(), + duration = duration, + status = if (success) AutomaticLRPGConsts.STATUS_SUCCESS else AutomaticLRPGConsts.STATUS_FAILED + ) + } + + // ์˜ค๋ž˜๋œ ๋ฉ”ํŠธ๋ฆญ ์ •๋ฆฌ (๋ฉ”๋ชจ๋ฆฌ ๋ˆ„์ˆ˜ ๋ฐฉ์ง€) + if (performanceMetrics.size > 10000) { + val sortedMetrics = performanceMetrics.values.sortedBy { it.startTime } + val toRemove = sortedMetrics.take(5000) + toRemove.forEach { performanceMetrics.remove(it.operationId) } + } + } + + private fun calculateCacheHitRate(): Double { + val totalRequests = expressionCache.values.sumOf { it.accessCount } + val cacheSize = expressionCache.size + return if (totalRequests > 0) cacheSize.toDouble() / totalRequests else 0.0 + } + + private fun getMemoryUsageInfo(): Map { + val runtime = Runtime.getRuntime() + val maxMemory = runtime.maxMemory() + val totalMemory = runtime.totalMemory() + val freeMemory = runtime.freeMemory() + val usedMemory = totalMemory - freeMemory + + return mapOf( + AutomaticLRPGConsts.MEM_MAX to maxMemory, + AutomaticLRPGConsts.MEM_TOTAL to totalMemory, + AutomaticLRPGConsts.MEM_USED to usedMemory, + AutomaticLRPGConsts.MEM_FREE to freeMemory, + AutomaticLRPGConsts.MEM_USED_PCT to (usedMemory.toDouble() / maxMemory * 100) + ) + } + + private fun getRecentPerformanceData(): List> { + return performanceMetrics.values + .sortedByDescending { it.startTime } + .take(100) + .map { metric -> + mapOf( + AutomaticLRPGConsts.RP_OPERATION_ID to metric.operationId, + AutomaticLRPGConsts.RP_OPERATION_TYPE to metric.operationType, + AutomaticLRPGConsts.RP_DURATION to metric.duration, + AutomaticLRPGConsts.RP_STATUS to metric.status, + AutomaticLRPGConsts.RP_TIMESTAMP to metric.startTime + ) + } + } + + private fun calculateEfficiency(processingTime: Long): Double { + // ํšจ์œจ์„ฑ = 1 / (์ฒ˜๋ฆฌ์‹œ๊ฐ„ / 1000) - ๊ฐ„๋‹จํ•œ ํšจ์œจ์„ฑ ์ง€ํ‘œ + return if (processingTime > 0) 1000.0 / processingTime else 1.0 + } + + private fun calculateResourceUtilization(): Double { + val memoryUsage = getMemoryUsageInfo() + val memoryUtilization = memoryUsage[AutomaticLRPGConsts.MEM_USED_PCT] as Double + val threadUtilization = (AutomaticLRPGConsts.THREAD_POOL_SIZE - 0) / AutomaticLRPGConsts.THREAD_POOL_SIZE.toDouble() * 100 // ๊ฐ„๋‹จํ•œ ๊ณ„์‚ฐ + + return (memoryUtilization + threadUtilization) / 2 + } + + private fun performAutomaticOptimization(issues: List): List { + val optimizations = mutableListOf() + + issues.forEach { issue -> + when { + issue.contains("ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„") -> { + optimizations.add(AutomaticLRPGConsts.AUTO_INC_CACHE) + } + issue.contains("์บ์‹œ ์ ์ค‘๋ฅ ") -> { + expressionCache.clear() + optimizations.add(AutomaticLRPGConsts.AUTO_CLEAR_CACHE) + } + issue.contains("๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋ฅ ") -> { + System.gc() + optimizations.add(AutomaticLRPGConsts.AUTO_GC) + } + } + } + + return optimizations + } + + /** + * ๋ฆฌ์†Œ์Šค ์ •๋ฆฌ + */ + fun shutdown() { + executorService.shutdown() + parsingTableCache.clear() + expressionCache.clear() + performanceMetrics.clear() + } + + // Data classes for complex results + + data class CompletableExpressionResult( + val isSuccess: Boolean, + val originalExpression: String = "", + val tokens: List = emptyList(), + val ast: ASTNode? = null, + val evaluationResult: EvaluationResult? = null, + val formattedResult: FormattedExpression? = null, + val validationResult: ValidationResult? = null, + val finalValidation: FinalValidationResult? = null, + val parsingMetadata: Map = emptyMap(), + val optimizationApplied: Boolean = false, + val validationApplied: Boolean = false, + val processingTime: Long = 0L, + val operationId: String = "", + val performanceMetrics: Map = emptyMap(), + val error: Throwable? = null, + val fromCache: Boolean = false + ) { + companion object { + fun success( + originalExpression: String, + tokens: List, + ast: ASTNode, + evaluationResult: EvaluationResult, + formattedResult: FormattedExpression, + validationResult: ValidationResult, + finalValidation: FinalValidationResult, + parsingMetadata: Map, + optimizationApplied: Boolean, + validationApplied: Boolean, + processingTime: Long, + operationId: String, + performanceMetrics: Map + ) = CompletableExpressionResult( + isSuccess = true, + originalExpression = originalExpression, + tokens = tokens, + ast = ast, + evaluationResult = evaluationResult, + formattedResult = formattedResult, + validationResult = validationResult, + finalValidation = finalValidation, + parsingMetadata = parsingMetadata, + optimizationApplied = optimizationApplied, + validationApplied = validationApplied, + processingTime = processingTime, + operationId = operationId, + performanceMetrics = performanceMetrics + ) + + fun failure( + error: Throwable, + processingTime: Long, + operationId: String + ) = CompletableExpressionResult( + isSuccess = false, + error = error, + processingTime = processingTime, + operationId = operationId + ) + } + } + + data class ValidationResult( + val isValid: Boolean, + val errors: List = emptyList() + ) { + companion object { + fun success() = ValidationResult(true) + } + } + + data class FinalValidationResult( + val isValid: Boolean, + val issues: List = emptyList() + ) { + companion object { + fun success() = FinalValidationResult(true) + } + } + + data class SystemDiagnosisResult( + val timestamp: Long, + val overallHealth: String, + val issues: List, + val recommendations: List, + val metrics: Map, + val optimizationApplied: List + ) + + data class ExpressionEvaluationResult( + val result: CompletableExpressionResult, + val timestamp: Long, + val accessCount: Int + ) { + fun toCompletableResult(fromCache: Boolean): CompletableExpressionResult { + return result.copy(fromCache = fromCache) + } + } + + data class PerformanceMetric( + val operationId: String, + val operationType: String, + val startTime: Long, + val endTime: Long = 0L, + val duration: Long = 0L, + val status: String = AutomaticLRPGConsts.STATUS_PENDING + ) + + /** + * AutomaticLRParserGenerator์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ƒ์ˆ˜ ๋ชจ์Œ + */ + companion object AutomaticLRPGConsts { + // Thread/Cache/Perf + const val THREAD_POOL_SIZE = 8 + const val MAX_EXPRESSION_CACHE_SIZE = 10000 + const val PERFORMANCE_THRESHOLD_MS = 1000 + const val MAX_CONCURRENT_EVALUATIONS = 100 + + // Cache keys & IDs + const val CACHE_KEY_LR1_PREFIX = "LR1_%d" + const val CACHE_KEY_LALR_PREFIX = "LALR_%d" + const val OP_ID_TEMPLATE = "OP_%d_%d" + + // Status strings + const val STATUS_PENDING = "PENDING" + const val STATUS_RUNNING = "RUNNING" + const val STATUS_SUCCESS = "SUCCESS" + const val STATUS_FAILED = "FAILED" + const val HEALTH_HEALTHY = "HEALTHY" + const val HEALTH_WARNING = "WARNING" + const val HEALTH_CRITICAL = "CRITICAL" + + // Metric map keys (top-level) + const val M_TOTAL_EVAL = "totalEvaluations" + const val M_SUCCESSFUL_EVAL = "successfulEvaluations" + const val M_FAILED_EVAL = "failedEvaluations" + const val M_SUCCESS_RATE = "successRate" + const val M_AVG_PROC_TIME = "averageProcessingTime" + const val M_CACHE_STATS = "cacheStats" + const val M_CURRENT_LOAD = "currentLoad" + const val M_THREAD_POOL_STATUS = "threadPoolStatus" + const val M_MEMORY_USAGE = "memoryUsage" + const val M_RECENT_PERF = "recentPerformance" + + // Nested: cacheStats keys + const val C_PARSING_TABLE_CACHE_SIZE = "parsingTableCacheSize" + const val C_EXPRESSION_CACHE_SIZE = "expressionCacheSize" + const val C_CACHE_HIT_RATE = "cacheHitRate" + + // Nested: threadPoolStatus keys + const val T_ACTIVE_THREADS = "activeThreads" + const val T_PENDING_TASKS = "pendingTasks" + + // Memory usage keys + const val MEM_MAX = "maxMemory" + const val MEM_TOTAL = "totalMemory" + const val MEM_USED = "usedMemory" + const val MEM_FREE = "freeMemory" + const val MEM_USED_PCT = "usedPercentage" + + // Recent performance item keys + const val RP_OPERATION_ID = "operationId" + const val RP_OPERATION_TYPE = "operationType" + const val RP_DURATION = "duration" + const val RP_STATUS = "status" + const val RP_TIMESTAMP = "timestamp" + + // Extended parsing metadata keys + const val X_PARSER_SERVICE_STATS = "parserServiceStats" + const val X_PARSER_SERVICE_CONFIG = "parserServiceConfig" + const val X_PARSING_TRACE = "parsingTrace" + + // Perf metrics map keys (createPerformanceMetrics) + const val PM_START_TIME = "startTime" + const val PM_TOTAL_TIME = "totalTime" + const val PM_THROUGHPUT = "throughput" + const val PM_EFFICIENCY = "efficiency" + const val PM_RESOURCE_UTIL = "resourceUtilization" + + // Diagnosis messages / triggers + const val ISSUE_AVG_TIME = "ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„์ด ์ž„๊ณ„๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: %sms > %sms" + const val ISSUE_CACHE_HIT = "์บ์‹œ ์ ์ค‘๋ฅ ์ด ๋‚ฎ์Šต๋‹ˆ๋‹ค: %s%%" + const val ISSUE_MEM_USAGE = "๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋ฅ ์ด ๋†’์Šต๋‹ˆ๋‹ค: %s%%" + + const val RECO_ENABLE_OPT_OR_CACHE = "LALR ์ตœ์ ํ™” ํ™œ์„ฑํ™” ๋˜๋Š” ์บ์‹œ ํฌ๊ธฐ ์ฆ๊ฐ€๋ฅผ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”" + const val RECO_INCREASE_OR_TUNE_CACHE = "์บ์‹œ ํฌ๊ธฐ๋ฅผ ๋Š˜๋ฆฌ๊ฑฐ๋‚˜ ์บ์‹œ ์ •์ฑ…์„ ์กฐ์ •ํ•ด๋ณด์„ธ์š”" + const val RECO_GC_OR_SHRINK_CACHE = "๊ฐ€๋ฒ ์ง€ ์ปฌ๋ ‰์…˜ ์ˆ˜ํ–‰ ๋˜๋Š” ์บ์‹œ ํฌ๊ธฐ ์ถ•์†Œ๋ฅผ ๊ณ ๋ คํ•ด๋ณด์„ธ์š”" + + // Automatic optimization notes + const val AUTO_INC_CACHE = "์บ์‹œ ํฌ๊ธฐ ์ž๋™ ์ฆ๊ฐ€" + const val AUTO_CLEAR_CACHE = "์บ์‹œ ์ •๋ฆฌ ์ˆ˜ํ–‰" + const val AUTO_GC = "๊ฐ€๋ฒ ์ง€ ์ปฌ๋ ‰์…˜ ์ˆ˜ํ–‰" + + const val COMPLETE_EXPRESSION_PROCESSING = "CompleteExpressionProcessing" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/LRParser.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/LRParser.kt new file mode 100644 index 00000000..97de5d2b --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/LRParser.kt @@ -0,0 +1,1102 @@ +package hs.kr.entrydsm.domain.parser.aggregates + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.values.Grammar +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import hs.kr.entrydsm.global.values.Position +import java.util.* + +/** + * LR(1) ํŒŒ์„œ ๊ตฌํ˜„์ฒด์ธ ์ง‘ํ•ฉ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค. + * + * ์ฃผ์–ด์ง„ ํ† ํฐ ์‹œํ€€์Šค๋ฅผ ๋ฌธ๋ฒ•์— ๋”ฐ๋ผ ํŒŒ์‹ฑํ•˜์—ฌ ์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ(AST)๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * LR(1) ํŒŒ์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‚ฌ์šฉํ•˜์—ฌ ํ•˜ํ–ฅ์‹ ๊ตฌ๋ฌธ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•˜๋ฉฐ, + * ๋ฌธ๋ฒ• ๊ทœ์น™๊ณผ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •ํ™•ํ•œ ๊ตฌ๋ฌธ ๋ถ„์„์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @property grammar ์‚ฌ์šฉํ•  ๋ฌธ๋ฒ• ๊ทœ์น™ + * @property maxStackSize ์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ + * @property enableLogging ๋กœ๊น… ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Aggregate(context = "parser") +class LRParser( + private val grammar: Grammar = Grammar, + private val maxStackSize: Int = 1000, + private val enableLogging: Boolean = false +) { + + // ํŒŒ์‹ฑ ์ƒํƒœ ์Šคํƒ + private val stateStack = Stack() + + // ์‹ฌ๋ณผ ์Šคํƒ + private val symbolStack = Stack() + + // ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” (๋‹จ์ˆœํ™”๋œ ๊ตฌํ˜„) + private val parsingTable = buildParsingTable() + + // ํŒŒ์‹ฑ ํ†ต๊ณ„ + private var parseCount = 0 + private var totalParseTime = 0L + private var successCount = 0 + private var failureCount = 0 + + /** + * ํ† ํฐ ์‹œํ€€์Šค๋ฅผ ํŒŒ์‹ฑํ•˜์—ฌ AST๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ํŒŒ์‹ฑํ•  ํ† ํฐ๋“ค + * @return ์ƒ์„ฑ๋œ AST ๋…ธ๋“œ + * @throws ParserException ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun parse(tokens: List): ASTNode { + return try { + val startTime = System.currentTimeMillis() + parseCount++ + + // ์ดˆ๊ธฐํ™” + stateStack.clear() + symbolStack.clear() + stateStack.push(0) // ์‹œ์ž‘ ์ƒํƒœ + + // ํ† ํฐ ์ธ๋ฑ์Šค + var tokenIndex = 0 + val tokenList = tokens.toMutableList() + + // EOF ํ† ํฐ ์ถ”๊ฐ€ + if (tokenList.isEmpty() || tokenList.last().type != TokenType.DOLLAR) { + tokenList.add(Token.eof()) + } + + while (tokenIndex < tokenList.size) { + val currentToken = tokenList[tokenIndex] + val currentState = stateStack.peek() + + if (enableLogging) { + logParsingState(currentState, currentToken, tokenIndex) + } + + // ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์—์„œ ์•ก์…˜ ์กฐํšŒ + val action = getAction(currentState, currentToken.type) + + when (action) { + is LRAction.Shift -> { + // Shift ์•ก์…˜: ํ† ํฐ์„ ์Šคํƒ์— ํ‘ธ์‹œํ•˜๊ณ  ๋‹ค์Œ ์ƒํƒœ๋กœ ์ „์ด + symbolStack.push(currentToken) + stateStack.push(action.state) + tokenIndex++ + } + + is LRAction.Reduce -> { + // Reduce ์•ก์…˜: ์ƒ์„ฑ ๊ทœ์น™์„ ์ ์šฉํ•˜์—ฌ ์Šคํƒ์„ ์ค„์ž„ + val production = action.production + val popCount = production.length + + // ์Šคํƒ์—์„œ ์‹ฌ๋ณผ๋“ค์„ ํŒ + val children = mutableListOf() + repeat(popCount) { + if (symbolStack.isNotEmpty()) { + children.add(0, symbolStack.pop()) + } + if (stateStack.isNotEmpty()) { + stateStack.pop() + } + } + + // AST ๋…ธ๋“œ ์ƒ์„ฑ + val astNode = production.buildAST(children) + symbolStack.push(astNode) + + // GOTO ์ „์ด + val gotoState = getGoto(stateStack.peek(), production.left) + stateStack.push(gotoState) + } + + is LRAction.Accept -> { + // Accept ์•ก์…˜: ํŒŒ์‹ฑ ์™„๋ฃŒ + val result = symbolStack.peek() + + val endTime = System.currentTimeMillis() + totalParseTime += (endTime - startTime) + successCount++ + + return if (result is ASTNode) { + result + } else { + throw ParserException.invalidASTNode(result) + } + } + + is LRAction.Error -> { + // Error ์•ก์…˜: ํŒŒ์‹ฑ ์˜ค๋ฅ˜ + failureCount++ + throw ParserException.syntaxError(currentToken, currentState, action.getFullErrorMessage()) + } + } + + // ์Šคํƒ ์˜ค๋ฒ„ํ”Œ๋กœ ๋ฐฉ์ง€ + if (stateStack.size > maxStackSize) { + throw ParserException.stackOverflow(maxStackSize) + } + } + + // ํŒŒ์‹ฑ์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + throw ParserException.incompleteInput() + + } catch (e: ParserException) { + failureCount++ + throw e + } catch (e: Exception) { + failureCount++ + throw ParserException.parsingError(e) + } + } + + /** + * ์ˆ˜์‹ ๋ฌธ์ž์—ด์„ ์ง์ ‘ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ํŒŒ์‹ฑํ•  ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @return ์ƒ์„ฑ๋œ AST ๋…ธ๋“œ + * @throws ParserException ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ + */ + fun parseFormula(formula: String): ASTNode { + // ๊ฐ„๋‹จํ•œ ํ† ํฐํ™” (์‹ค์ œ๋กœ๋Š” ๋ ‰์„œ ์‚ฌ์šฉ) + val tokens = tokenizeFormula(formula) + return parse(tokens) + } + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์—์„œ ์•ก์…˜์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ํ˜„์žฌ ์ƒํƒœ + * @param tokenType ํ† ํฐ ํƒ€์ž… + * @return LR ์•ก์…˜ + */ + private fun getAction(state: Int, tokenType: TokenType): LRAction { + val key = Pair(state, tokenType) + return parsingTable[key] ?: LRAction.Error("PARSE_ERROR", "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ํ† ํฐ: $tokenType in state $state") + } + + /** + * GOTO ํ…Œ์ด๋ธ”์—์„œ ๋‹ค์Œ ์ƒํƒœ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ํ˜„์žฌ ์ƒํƒœ + * @param nonTerminal ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @return ๋‹ค์Œ ์ƒํƒœ + */ + private fun getGoto(state: Int, nonTerminal: TokenType): Int { + // ๊ฐ„๋‹จํ•œ GOTO ํ…Œ์ด๋ธ” ๊ตฌํ˜„ + return when (Pair(state, nonTerminal)) { + // ์ƒํƒœ 0์—์„œ์˜ ์ „์ด (์‹œ์ž‘ ์ƒํƒœ) + Pair(0, TokenType.EXPR) -> 1 + Pair(0, TokenType.AND_EXPR) -> 2 + Pair(0, TokenType.COMP_EXPR) -> 3 + Pair(0, TokenType.ARITH_EXPR) -> 4 + Pair(0, TokenType.TERM) -> 22 + Pair(0, TokenType.FACTOR) -> 23 + Pair(0, TokenType.PRIMARY) -> 24 + + // ์ƒํƒœ 7 (LEFT_PAREN ํ›„)์—์„œ์˜ ์ „์ด + Pair(7, TokenType.EXPR) -> 25 + Pair(7, TokenType.AND_EXPR) -> 2 + Pair(7, TokenType.COMP_EXPR) -> 3 + Pair(7, TokenType.ARITH_EXPR) -> 4 + Pair(7, TokenType.TERM) -> 22 + Pair(7, TokenType.FACTOR) -> 23 + Pair(7, TokenType.PRIMARY) -> 24 + + // ์ƒํƒœ 8 (MINUS ํ›„)์—์„œ์˜ ์ „์ด - ๋‹จํ•ญ ๋งˆ์ด๋„ˆ์Šค + Pair(8, TokenType.PRIMARY) -> 26 + + // ์ƒํƒœ 11 (OR ํ›„)์—์„œ์˜ ์ „์ด + Pair(11, TokenType.AND_EXPR) -> 27 + Pair(11, TokenType.COMP_EXPR) -> 3 + Pair(11, TokenType.ARITH_EXPR) -> 4 + Pair(11, TokenType.TERM) -> 22 + Pair(11, TokenType.FACTOR) -> 23 + Pair(11, TokenType.PRIMARY) -> 24 + + // ์ƒํƒœ 12 (AND ํ›„)์—์„œ์˜ ์ „์ด + Pair(12, TokenType.COMP_EXPR) -> 28 + Pair(12, TokenType.ARITH_EXPR) -> 4 + Pair(12, TokenType.TERM) -> 22 + Pair(12, TokenType.FACTOR) -> 23 + Pair(12, TokenType.PRIMARY) -> 24 + + // ๋น„๊ต ์—ฐ์‚ฐ์ž ํ›„ ์ „์ด๋“ค (์ƒํƒœ 13-18) + Pair(13, TokenType.ARITH_EXPR) -> 29 // EQUAL ํ›„ + Pair(13, TokenType.TERM) -> 22 // ARITH_EXPR๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด TERM๋„ ํ•„์š” + Pair(13, TokenType.FACTOR) -> 23 // TERM์œผ๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด FACTOR๋„ ํ•„์š” + Pair(13, TokenType.PRIMARY) -> 24 // FACTOR๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด PRIMARY๋„ ํ•„์š” + Pair(14, TokenType.ARITH_EXPR) -> 30 // NOT_EQUAL ํ›„ + Pair(14, TokenType.TERM) -> 22 + Pair(14, TokenType.FACTOR) -> 23 + Pair(14, TokenType.PRIMARY) -> 24 + Pair(15, TokenType.ARITH_EXPR) -> 31 // LESS ํ›„ + Pair(15, TokenType.TERM) -> 22 + Pair(15, TokenType.FACTOR) -> 23 + Pair(15, TokenType.PRIMARY) -> 24 + Pair(16, TokenType.ARITH_EXPR) -> 32 // LESS_EQUAL ํ›„ + Pair(16, TokenType.TERM) -> 22 + Pair(16, TokenType.FACTOR) -> 23 + Pair(16, TokenType.PRIMARY) -> 24 + Pair(17, TokenType.ARITH_EXPR) -> 33 // GREATER ํ›„ + Pair(17, TokenType.TERM) -> 22 + Pair(17, TokenType.FACTOR) -> 23 + Pair(17, TokenType.PRIMARY) -> 24 + Pair(18, TokenType.ARITH_EXPR) -> 34 // GREATER_EQUAL ํ›„ + Pair(18, TokenType.TERM) -> 22 + Pair(18, TokenType.FACTOR) -> 23 + Pair(18, TokenType.PRIMARY) -> 24 + + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž ํ›„ ์ „์ด๋“ค (์ƒํƒœ 19-20) + Pair(19, TokenType.TERM) -> 35 // PLUS ํ›„ + Pair(19, TokenType.FACTOR) -> 23 // TERM์œผ๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด FACTOR๋„ ํ•„์š” + Pair(19, TokenType.PRIMARY) -> 24 // FACTOR๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด PRIMARY๋„ ํ•„์š” + Pair(20, TokenType.TERM) -> 36 // MINUS ํ›„ + Pair(20, TokenType.FACTOR) -> 23 // TERM์œผ๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด FACTOR๋„ ํ•„์š” + Pair(20, TokenType.PRIMARY) -> 24 // FACTOR๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด PRIMARY๋„ ํ•„์š” + + // ๊ณฑ์…ˆ/๋‚˜๋ˆ—์…ˆ/๋ชจ๋“ˆ๋กœ ์—ฐ์‚ฐ์ž ํ›„ ์ „์ด๋“ค (์ƒํƒœ 37-39) + Pair(37, TokenType.FACTOR) -> 44 // MULTIPLY ํ›„ + Pair(37, TokenType.PRIMARY) -> 24 // FACTOR๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด PRIMARY๋„ ํ•„์š” + Pair(38, TokenType.FACTOR) -> 45 // DIVIDE ํ›„ + Pair(38, TokenType.PRIMARY) -> 24 // FACTOR๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด PRIMARY๋„ ํ•„์š” + Pair(39, TokenType.FACTOR) -> 46 // MODULO ํ›„ + Pair(39, TokenType.PRIMARY) -> 24 // FACTOR๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด PRIMARY๋„ ํ•„์š” + + // ๊ฑฐ๋“ญ์ œ๊ณฑ ์—ฐ์‚ฐ์ž ํ›„ ์ „์ด (์ƒํƒœ 40) + Pair(40, TokenType.FACTOR) -> 47 // POWER ํ›„ + Pair(40, TokenType.PRIMARY) -> 24 // FACTOR๋กœ ๊ฐ€๊ธฐ ์œ„ํ•ด PRIMARY๋„ ํ•„์š” + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ด€๋ จ (์ƒํƒœ 21) + Pair(21, TokenType.ARGS) -> 41 + Pair(21, TokenType.EXPR) -> 42 + + // IF ์ฒ˜๋ฆฌ ๊ด€๋ จ GOTO ์ „์ด๋“ค + Pair(49, TokenType.EXPR) -> 50 // IF ( ํ›„ ์ฒซ ๋ฒˆ์งธ EXPR + Pair(49, TokenType.AND_EXPR) -> 2 + Pair(49, TokenType.COMP_EXPR) -> 3 + Pair(49, TokenType.ARITH_EXPR) -> 4 + Pair(49, TokenType.TERM) -> 22 + Pair(49, TokenType.FACTOR) -> 23 + Pair(49, TokenType.PRIMARY) -> 24 + + Pair(51, TokenType.EXPR) -> 52 // IF ( EXPR , ํ›„ ๋‘ ๋ฒˆ์งธ EXPR + Pair(51, TokenType.AND_EXPR) -> 2 + Pair(51, TokenType.COMP_EXPR) -> 3 + Pair(51, TokenType.ARITH_EXPR) -> 4 + Pair(51, TokenType.TERM) -> 22 + Pair(51, TokenType.FACTOR) -> 23 + Pair(51, TokenType.PRIMARY) -> 24 + + Pair(53, TokenType.EXPR) -> 54 // IF ( EXPR , EXPR , ํ›„ ์„ธ ๋ฒˆ์งธ EXPR + Pair(53, TokenType.AND_EXPR) -> 2 + Pair(53, TokenType.COMP_EXPR) -> 3 + Pair(53, TokenType.ARITH_EXPR) -> 4 + Pair(53, TokenType.TERM) -> 22 + Pair(53, TokenType.FACTOR) -> 23 + Pair(53, TokenType.PRIMARY) -> 24 + + else -> state + } + } + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๋งต + */ + private fun buildParsingTable(): Map, LRAction> { + val table = mutableMapOf, LRAction>() + + // ๊ฐ„๋‹จํ•œ LR(1) ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ตฌํ˜„ (Grammar์˜ Production๊ณผ ์ผ์น˜) + + // ์ƒํƒœ 0: ์‹œ์ž‘ ์ƒํƒœ + table[Pair(0, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(0, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(0, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(0, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(0, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(0, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(0, TokenType.IF)] = LRAction.Shift(48) // IF ํ‚ค์›Œ๋“œ + + // ์ƒํƒœ 1: EXPR ์™„๋ฃŒ ์ƒํƒœ + table[Pair(1, TokenType.DOLLAR)] = LRAction.Accept + table[Pair(1, TokenType.OR)] = LRAction.Shift(11) // EXPR โ†’ EXPR || AND_EXPR + table[Pair(1, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(1)) + table[Pair(1, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(1)) + + // ์ƒํƒœ 2: AND_EXPR ์™„๋ฃŒ ์ƒํƒœ + table[Pair(2, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(1)) // EXPR โ†’ AND_EXPR + table[Pair(2, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(1)) + table[Pair(2, TokenType.AND)] = LRAction.Shift(12) // AND_EXPR โ†’ AND_EXPR && COMP_EXPR + table[Pair(2, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(1)) + table[Pair(2, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(1)) + + // ์ƒํƒœ 3: COMP_EXPR ์™„๋ฃŒ ์ƒํƒœ + table[Pair(3, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(3)) // AND_EXPR โ†’ COMP_EXPR + table[Pair(3, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(3)) + table[Pair(3, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(3)) + table[Pair(3, TokenType.EQUAL)] = LRAction.Shift(13) // ๋น„๊ต ์—ฐ์‚ฐ์ž๋“ค + table[Pair(3, TokenType.NOT_EQUAL)] = LRAction.Shift(14) + table[Pair(3, TokenType.LESS)] = LRAction.Shift(15) + table[Pair(3, TokenType.LESS_EQUAL)] = LRAction.Shift(16) + table[Pair(3, TokenType.GREATER)] = LRAction.Shift(17) + table[Pair(3, TokenType.GREATER_EQUAL)] = LRAction.Shift(18) + table[Pair(3, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(3)) + table[Pair(3, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(3)) + + // ์ƒํƒœ 4: ARITH_EXPR ์™„๋ฃŒ ์ƒํƒœ + table[Pair(4, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(10)) // COMP_EXPR โ†’ ARITH_EXPR + table[Pair(4, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(10)) + table[Pair(4, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(10)) + table[Pair(4, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(10)) + table[Pair(4, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(10)) + table[Pair(4, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(10)) + table[Pair(4, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(10)) + table[Pair(4, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(10)) + table[Pair(4, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(10)) + table[Pair(4, TokenType.PLUS)] = LRAction.Shift(19) // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž๋“ค + table[Pair(4, TokenType.MINUS)] = LRAction.Shift(20) + table[Pair(4, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(10)) + table[Pair(4, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(10)) + + // ์ƒํƒœ 5: NUMBER โ†’ reduce to PRIMARY + table[Pair(5, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(24)) // PRIMARY โ†’ NUMBER + table[Pair(5, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.POWER)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(24)) + table[Pair(5, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(24)) + + // ์ƒํƒœ 6: IDENTIFIER โ†’ reduce to PRIMARY or function call + table[Pair(6, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(26)) // PRIMARY โ†’ IDENTIFIER + table[Pair(6, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.POWER)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.LEFT_PAREN)] = LRAction.Shift(21) // ํ•จ์ˆ˜ ํ˜ธ์ถœ + table[Pair(6, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(26)) + table[Pair(6, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(26)) + + // ์ƒํƒœ 7: LEFT_PAREN ํ›„ - ๊ด„ํ˜ธ ์•ˆ ํ‘œํ˜„์‹ ์‹œ์ž‘ + table[Pair(7, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(7, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(7, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(7, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(7, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(7, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(7, TokenType.IF)] = LRAction.Shift(48) // IF ํ‚ค์›Œ๋“œ + + // ์ƒํƒœ 8: MINUS ํ›„ - ๋‹จํ•ญ ๋งˆ์ด๋„ˆ์Šค + table[Pair(8, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(8, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(8, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(8, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(8, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(8, TokenType.IF)] = LRAction.Shift(48) // IF ํ‚ค์›Œ๋“œ + + // ์ƒํƒœ 9: TRUE โ†’ reduce to PRIMARY + table[Pair(9, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(27)) // PRIMARY โ†’ TRUE + table[Pair(9, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.POWER)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(27)) + table[Pair(9, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(27)) + + // ์ƒํƒœ 10: FALSE โ†’ reduce to PRIMARY + table[Pair(10, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(28)) // PRIMARY โ†’ FALSE + table[Pair(10, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.POWER)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(28)) + table[Pair(10, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(28)) + + // ์ƒํƒœ 11: OR ํ›„ ์ƒํƒœ - AND_EXPR ํ•„์š” + table[Pair(11, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(11, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(11, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(11, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(11, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(11, TokenType.FALSE)] = LRAction.Shift(10) + + // ์ƒํƒœ 12: AND ํ›„ ์ƒํƒœ - COMP_EXPR ํ•„์š” + table[Pair(12, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(12, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(12, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(12, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(12, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(12, TokenType.FALSE)] = LRAction.Shift(10) + + // ์ƒํƒœ 13-18: ๋น„๊ต ์—ฐ์‚ฐ์ž ํ›„ ์ƒํƒœ๋“ค - ARITH_EXPR ํ•„์š” + for (state in 13..18) { + table[Pair(state, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(state, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(state, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(state, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(state, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(state, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(state, TokenType.IF)] = LRAction.Shift(48) // IF ํ‚ค์›Œ๋“œ + } + + // ์ƒํƒœ 19-20: ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž ํ›„ ์ƒํƒœ๋“ค - TERM ํ•„์š” + for (state in 19..20) { + table[Pair(state, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(state, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(state, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(state, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(state, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(state, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(state, TokenType.IF)] = LRAction.Shift(48) // IF ํ‚ค์›Œ๋“œ + } + + // ์ƒํƒœ 21: ํ•จ์ˆ˜ ํ˜ธ์ถœ ์‹œ์ž‘ - IDENTIFIER ( ํ›„ + table[Pair(21, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(21, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(21, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(21, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(21, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(21, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(21, TokenType.IF)] = LRAction.Shift(48) // IF ํ‚ค์›Œ๋“œ + table[Pair(21, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(30)) // PRIMARY โ†’ IDENTIFIER ( ) + + // ์ƒํƒœ 22: TERM ์™„๋ฃŒ ์ƒํƒœ + table[Pair(22, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(13)) // ARITH_EXPR โ†’ TERM + table[Pair(22, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.MULTIPLY)] = LRAction.Shift(37) // TERM * FACTOR + table[Pair(22, TokenType.DIVIDE)] = LRAction.Shift(38) // TERM / FACTOR + table[Pair(22, TokenType.MODULO)] = LRAction.Shift(39) // TERM % FACTOR + table[Pair(22, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(13)) + table[Pair(22, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(13)) + + // ์ƒํƒœ 23: FACTOR ์™„๋ฃŒ ์ƒํƒœ + table[Pair(23, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(17)) // TERM โ†’ FACTOR + table[Pair(23, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.MODULO)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(17)) + table[Pair(23, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(17)) + + // ์ƒํƒœ 24: PRIMARY ์™„๋ฃŒ ์ƒํƒœ + table[Pair(24, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(19)) // FACTOR โ†’ PRIMARY + table[Pair(24, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.MODULO)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.POWER)] = LRAction.Shift(40) // PRIMARY ^ FACTOR (์šฐ๊ฒฐํ•ฉ) + table[Pair(24, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(19)) + table[Pair(24, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(19)) + + // ์ƒํƒœ 25: ๊ด„ํ˜ธ ์•ˆ EXPR ์™„๋ฃŒ - RIGHT_PAREN ๊ธฐ๋Œ€ + table[Pair(25, TokenType.OR)] = LRAction.Shift(11) + table[Pair(25, TokenType.RIGHT_PAREN)] = LRAction.Shift(43) // PRIMARY โ†’ ( EXPR ) + + // ์ƒํƒœ 26: ๋‹จํ•ญ ๋งˆ์ด๋„ˆ์Šค ์™„๋ฃŒ - PRIMARY โ†’ - PRIMARY + table[Pair(26, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(21)) // PRIMARY โ†’ - PRIMARY + table[Pair(26, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.MODULO)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.POWER)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(21)) + table[Pair(26, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(21)) + + // ์ƒํƒœ 27-55: ๋‚˜๋จธ์ง€ ์ƒํƒœ๋“ค์˜ ๊ธฐ๋ณธ reduce ์•ก์…˜๋“ค + for (state in 27..55) { + // ๊ฐ ์ƒํƒœ๋ณ„๋กœ ์ ์ ˆํ•œ reduce ์•ก์…˜ ์ถ”๊ฐ€ + when (state) { + 27 -> { + // OR ํ›„ AND_EXPR ์™„๋ฃŒ - EXPR โ†’ EXPR || AND_EXPR + table[Pair(27, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(0)) + table[Pair(27, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(0)) + table[Pair(27, TokenType.AND)] = LRAction.Shift(12) + table[Pair(27, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(0)) + table[Pair(27, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(0)) + } + 28 -> { + // AND ํ›„ COMP_EXPR ์™„๋ฃŒ - AND_EXPR โ†’ AND_EXPR && COMP_EXPR + table[Pair(28, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(2)) + table[Pair(28, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(2)) + table[Pair(28, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(2)) + table[Pair(28, TokenType.EQUAL)] = LRAction.Shift(13) + table[Pair(28, TokenType.NOT_EQUAL)] = LRAction.Shift(14) + table[Pair(28, TokenType.LESS)] = LRAction.Shift(15) + table[Pair(28, TokenType.LESS_EQUAL)] = LRAction.Shift(16) + table[Pair(28, TokenType.GREATER)] = LRAction.Shift(17) + table[Pair(28, TokenType.GREATER_EQUAL)] = LRAction.Shift(18) + table[Pair(28, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(2)) + table[Pair(28, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(2)) + } + in 29..34 -> { + // ๋น„๊ต ์—ฐ์‚ฐ์ž ํ›„ ARITH_EXPR ์™„๋ฃŒ ์ƒํƒœ๋“ค + val productionId = when (state) { + 29 -> 4 // COMP_EXPR โ†’ COMP_EXPR == ARITH_EXPR + 30 -> 5 // COMP_EXPR โ†’ COMP_EXPR != ARITH_EXPR + 31 -> 6 // COMP_EXPR โ†’ COMP_EXPR < ARITH_EXPR + 32 -> 7 // COMP_EXPR โ†’ COMP_EXPR <= ARITH_EXPR + 33 -> 8 // COMP_EXPR โ†’ COMP_EXPR > ARITH_EXPR + 34 -> 9 // COMP_EXPR โ†’ COMP_EXPR >= ARITH_EXPR + else -> 4 + } + table[Pair(state, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.PLUS)] = LRAction.Shift(19) + table[Pair(state, TokenType.MINUS)] = LRAction.Shift(20) + table[Pair(state, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(productionId)) + } + in 35..36 -> { + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž ํ›„ TERM ์™„๋ฃŒ ์ƒํƒœ๋“ค + val productionId = when (state) { + 35 -> 11 // ARITH_EXPR โ†’ ARITH_EXPR + TERM + 36 -> 12 // ARITH_EXPR โ†’ ARITH_EXPR - TERM + else -> 11 + } + table[Pair(state, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.MULTIPLY)] = LRAction.Shift(37) + table[Pair(state, TokenType.DIVIDE)] = LRAction.Shift(38) + table[Pair(state, TokenType.MODULO)] = LRAction.Shift(39) + table[Pair(state, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(productionId)) + } + in 37..39 -> { + // ๊ณฑ์…ˆ/๋‚˜๋ˆ—์…ˆ/๋ชจ๋“ˆ๋กœ ์—ฐ์‚ฐ์ž ํ›„ ์ƒํƒœ๋“ค - FACTOR ํ•„์š” + table[Pair(state, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(state, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(state, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(state, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(state, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(state, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(state, TokenType.IF)] = LRAction.Shift(48) // IF ํ‚ค์›Œ๋“œ + } + 40 -> { + // POWER ์—ฐ์‚ฐ์ž ํ›„ ์ƒํƒœ - FACTOR ํ•„์š” (์šฐ๊ฒฐํ•ฉ) + table[Pair(40, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(40, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(40, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(40, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(40, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(40, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(40, TokenType.IF)] = LRAction.Shift(48) // IF ํ‚ค์›Œ๋“œ + } + 43 -> { + // ๊ด„ํ˜ธ ๋‹ซํž˜ ์™„๋ฃŒ - PRIMARY โ†’ ( EXPR ) + table[Pair(43, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.MODULO)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.POWER)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(20)) + table[Pair(43, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(20)) + } + in 44..47 -> { + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž ํ›„ FACTOR ์™„๋ฃŒ ์ƒํƒœ๋“ค + val productionId = when (state) { + 44 -> 14 // TERM โ†’ TERM * FACTOR + 45 -> 15 // TERM โ†’ TERM / FACTOR + 46 -> 16 // TERM โ†’ TERM % FACTOR + 47 -> 18 // FACTOR โ†’ PRIMARY ^ FACTOR + else -> 14 + } + table[Pair(state, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.MODULO)] = LRAction.Reduce(grammar.getProduction(productionId)) + if (state != 47) { // POWER๋Š” ์šฐ๊ฒฐํ•ฉ์ด๋ฏ€๋กœ ์ œ์™ธ + table[Pair(state, TokenType.POWER)] = LRAction.Reduce(grammar.getProduction(productionId)) + } + table[Pair(state, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(productionId)) + table[Pair(state, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(productionId)) + } + 48 -> { + // IF ํ‚ค์›Œ๋“œ ํ›„ ์ƒํƒœ - LEFT_PAREN ๊ธฐ๋Œ€ + table[Pair(48, TokenType.LEFT_PAREN)] = LRAction.Shift(49) + } + 49 -> { + // IF ( ํ›„ ์ƒํƒœ - ์ฒซ ๋ฒˆ์งธ EXPR ํ•„์š” + table[Pair(49, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(49, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(49, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(49, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(49, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(49, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(49, TokenType.IF)] = LRAction.Shift(48) + } + 50 -> { + // IF ( EXPR ํ›„ ์ƒํƒœ - COMMA ๊ธฐ๋Œ€ + table[Pair(50, TokenType.OR)] = LRAction.Shift(11) + table[Pair(50, TokenType.COMMA)] = LRAction.Shift(51) + } + 51 -> { + // IF ( EXPR , ํ›„ ์ƒํƒœ - ๋‘ ๋ฒˆ์งธ EXPR ํ•„์š” + table[Pair(51, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(51, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(51, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(51, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(51, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(51, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(51, TokenType.IF)] = LRAction.Shift(48) + } + 52 -> { + // IF ( EXPR , EXPR ํ›„ ์ƒํƒœ - COMMA ๊ธฐ๋Œ€ + table[Pair(52, TokenType.OR)] = LRAction.Shift(11) + table[Pair(52, TokenType.COMMA)] = LRAction.Shift(53) + } + 53 -> { + // IF ( EXPR , EXPR , ํ›„ ์ƒํƒœ - ์„ธ ๋ฒˆ์งธ EXPR ํ•„์š” + table[Pair(53, TokenType.NUMBER)] = LRAction.Shift(5) + table[Pair(53, TokenType.IDENTIFIER)] = LRAction.Shift(6) + table[Pair(53, TokenType.LEFT_PAREN)] = LRAction.Shift(7) + table[Pair(53, TokenType.MINUS)] = LRAction.Shift(8) + table[Pair(53, TokenType.TRUE)] = LRAction.Shift(9) + table[Pair(53, TokenType.FALSE)] = LRAction.Shift(10) + table[Pair(53, TokenType.IF)] = LRAction.Shift(48) + } + 54 -> { + // IF ( EXPR , EXPR , EXPR ํ›„ ์ƒํƒœ - RIGHT_PAREN ๊ธฐ๋Œ€ + table[Pair(54, TokenType.OR)] = LRAction.Shift(11) + table[Pair(54, TokenType.RIGHT_PAREN)] = LRAction.Shift(55) + } + 55 -> { + // IF ( EXPR , EXPR , EXPR ) ์™„๋ฃŒ - PRIMARY โ†’ IF ( EXPR , EXPR , EXPR ) + table[Pair(55, TokenType.DOLLAR)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.OR)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.AND)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.EQUAL)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.NOT_EQUAL)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.LESS)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.LESS_EQUAL)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.GREATER)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.GREATER_EQUAL)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.PLUS)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.MINUS)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.MULTIPLY)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.DIVIDE)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.MODULO)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.POWER)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.RIGHT_PAREN)] = LRAction.Reduce(grammar.getProduction(31)) + table[Pair(55, TokenType.COMMA)] = LRAction.Reduce(grammar.getProduction(31)) + } + } + } + + return table + } + + /** + * ์ˆ˜์‹์„ ๊ฐ„๋‹จํžˆ ํ† ํฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param formula ์ˆ˜์‹ ๋ฌธ์ž์—ด + * @return ํ† ํฐ ๋ฆฌ์ŠคํŠธ + */ + private fun tokenizeFormula(formula: String): List { + val tokens = mutableListOf() + var i = 0 + + while (i < formula.length) { + val char = formula[i] + + when { + char.isWhitespace() -> i++ + char.isDigit() -> { + val start = i + while (i < formula.length && (formula[i].isDigit() || formula[i] == '.')) { + i++ + } + val number = formula.substring(start, i) + tokens.add(Token(TokenType.NUMBER, number, Position.of(start))) + } + char.isLetter() -> { + val start = i + while (i < formula.length && (formula[i].isLetterOrDigit() || formula[i] == '_')) { + i++ + } + val identifier = formula.substring(start, i) + val tokenType = when (identifier.lowercase()) { + "true" -> TokenType.TRUE + "false" -> TokenType.FALSE + "if" -> TokenType.IF + else -> TokenType.IDENTIFIER + } + tokens.add(Token(tokenType, identifier, Position.of(start))) + } + char == '+' -> { + tokens.add(Token(TokenType.PLUS, "+", Position.of(i))) + i++ + } + char == '-' -> { + tokens.add(Token(TokenType.MINUS, "-", Position.of(i))) + i++ + } + char == '*' -> { + tokens.add(Token(TokenType.MULTIPLY, "*", Position.of(i))) + i++ + } + char == '/' -> { + tokens.add(Token(TokenType.DIVIDE, "/", Position.of(i))) + i++ + } + char == '^' -> { + tokens.add(Token(TokenType.POWER, "^", Position.of(i))) + i++ + } + char == '(' -> { + tokens.add(Token(TokenType.LEFT_PAREN, "(", Position.of(i))) + i++ + } + char == ')' -> { + tokens.add(Token(TokenType.RIGHT_PAREN, ")", Position.of(i))) + i++ + } + char == ',' -> { + tokens.add(Token(TokenType.COMMA, ",", Position.of(i))) + i++ + } + char == '=' && i + 1 < formula.length && formula[i + 1] == '=' -> { + tokens.add(Token(TokenType.EQUAL, "==", Position.of(i))) + i += 2 + } + char == '!' && i + 1 < formula.length && formula[i + 1] == '=' -> { + tokens.add(Token(TokenType.NOT_EQUAL, "!=", Position.of(i))) + i += 2 + } + char == '<' && i + 1 < formula.length && formula[i + 1] == '=' -> { + tokens.add(Token(TokenType.LESS_EQUAL, "<=", Position.of(i))) + i += 2 + } + char == '>' && i + 1 < formula.length && formula[i + 1] == '=' -> { + tokens.add(Token(TokenType.GREATER_EQUAL, ">=", Position.of(i))) + i += 2 + } + char == '<' -> { + tokens.add(Token(TokenType.LESS, "<", Position.of(i))) + i++ + } + char == '>' -> { + tokens.add(Token(TokenType.GREATER, ">", Position.of(i))) + i++ + } + char == '&' && i + 1 < formula.length && formula[i + 1] == '&' -> { + tokens.add(Token(TokenType.AND, "&&", Position.of(i))) + i += 2 + } + char == '|' && i + 1 < formula.length && formula[i + 1] == '|' -> { + tokens.add(Token(TokenType.OR, "||", Position.of(i))) + i += 2 + } + char == '!' -> { + tokens.add(Token(TokenType.NOT, "!", Position.of(i))) + i++ + } + else -> { + i++ // ์•Œ ์ˆ˜ ์—†๋Š” ๋ฌธ์ž๋Š” ๋ฌด์‹œ + } + } + } + + return tokens + } + + /** + * ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ๋กœ๊น…ํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ํ˜„์žฌ ์ƒํƒœ + * @param token ํ˜„์žฌ ํ† ํฐ + * @param tokenIndex ํ† ํฐ ์ธ๋ฑ์Šค + */ + private fun logParsingState(state: Int, token: Token, tokenIndex: Int) { + println("Parse State: $state, Token: ${token.type}(${token.value}), Index: $tokenIndex") + println("State Stack: $stateStack") + println("Symbol Stack: ${symbolStack.map { + when (it) { + is Token -> "${it.type}(${it.value})" + is ASTNode -> it.javaClass.simpleName + else -> it.toString() + } + }}") + println("---") + } + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ์ถฉ๋Œ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ถฉ๋Œ ์ •๋ณด ๋งต + */ + fun checkTableConflicts(): Map { + val conflicts = mutableListOf() + val shiftReduceConflicts = mutableListOf() + val reduceReduceConflicts = mutableListOf() + + // ๊ฐ ์ƒํƒœ๋ณ„๋กœ ์ถฉ๋Œ ํ™•์ธ + val stateGroups = parsingTable.keys.groupBy { it.first } + + for ((state, entries) in stateGroups) { + val tokenGroups = entries.groupBy { it.second } + + for ((token, stateTokenPairs) in tokenGroups) { + if (stateTokenPairs.size > 1) { + val actions = stateTokenPairs.map { parsingTable[it]!! } + + if (LRAction.hasShiftReduceConflict(actions)) { + shiftReduceConflicts.add("State $state, Token $token") + } + + if (LRAction.hasReduceReduceConflict(actions)) { + reduceReduceConflicts.add("State $state, Token $token") + } + + conflicts.add("State $state, Token $token: ${actions.map { it.getActionType() }}") + } + } + } + + return mapOf( + "totalConflicts" to conflicts.size, + "shiftReduceConflicts" to shiftReduceConflicts.size, + "reduceReduceConflicts" to reduceReduceConflicts.size, + "conflicts" to conflicts, + "shiftReduceDetails" to shiftReduceConflicts, + "reduceReduceDetails" to reduceReduceConflicts + ) + } + + /** + * ํŒŒ์„œ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์„œ ํ†ต๊ณ„ ๋งต + */ + fun getParserStatistics(): Map = mapOf( + "parseCount" to parseCount, + "successCount" to successCount, + "failureCount" to failureCount, + "successRate" to if (parseCount > 0) successCount.toDouble() / parseCount else 0.0, + "totalParseTime" to totalParseTime, + "averageParseTime" to if (parseCount > 0) totalParseTime.toDouble() / parseCount else 0.0, + "maxStackSize" to maxStackSize, + "grammarProductions" to grammar.productions.size, + "parsingTableSize" to parsingTable.size, + "enableLogging" to enableLogging + ) + + /** + * ํŒŒ์„œ ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์„œ ์„ค์ • ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxStackSize" to maxStackSize, + "enableLogging" to enableLogging, + "grammarValid" to grammar.isValid(), + "grammarProductions" to grammar.productions.size, + "parsingTableSize" to parsingTable.size, + "supportedTokenTypes" to TokenType.values().map { it.name } + ) + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ๋ฌธ์ž์—ด๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๋ฌธ์ž์—ด + */ + fun printParsingTable(): String = buildString { + appendLine("=== LR(1) ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ===") + appendLine("์ด ${parsingTable.size}๊ฐœ์˜ ์—”ํŠธ๋ฆฌ") + appendLine() + + val stateGroups = parsingTable.entries.groupBy { it.key.first } + + for ((state, entries) in stateGroups.toSortedMap()) { + appendLine("์ƒํƒœ $state:") + for ((key, action) in entries.sortedBy { it.key.second.name }) { + appendLine(" ${key.second} -> $action") + } + appendLine() + } + } + + /** + * ํŒŒ์„œ๋ฅผ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun reset() { + stateStack.clear() + symbolStack.clear() + parseCount = 0 + totalParseTime = 0L + successCount = 0 + failureCount = 0 + } + + /** + * ์ƒˆ๋กœ์šด ์„ค์ •์œผ๋กœ ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newGrammar ์ƒˆ๋กœ์šด ๋ฌธ๋ฒ• + * @param newMaxStackSize ์ƒˆ๋กœ์šด ์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ + * @param newEnableLogging ์ƒˆ๋กœ์šด ๋กœ๊น… ์„ค์ • + * @return ์ƒˆ๋กœ์šด LRParser ์ธ์Šคํ„ด์Šค + */ + fun withConfiguration( + newGrammar: Grammar = grammar, + newMaxStackSize: Int = maxStackSize, + newEnableLogging: Boolean = enableLogging + ): LRParser { + return LRParser(newGrammar, newMaxStackSize, newEnableLogging) + } + + companion object { + /** + * ๊ธฐ๋ณธ ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return LRParser ์ธ์Šคํ„ด์Šค + */ + fun createDefault(): LRParser = LRParser() + + /** + * ๋””๋ฒ„๊น… ๋ชจ๋“œ ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋””๋ฒ„๊น… ๋ชจ๋“œ LRParser ์ธ์Šคํ„ด์Šค + */ + fun createDebug(): LRParser = LRParser(enableLogging = true) + + /** + * ๊ณ ์„ฑ๋Šฅ ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณ ์„ฑ๋Šฅ LRParser ์ธ์Šคํ„ด์Šค + */ + fun createHighPerformance(): LRParser = LRParser(maxStackSize = 10000, enableLogging = false) + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param grammar ๋ฌธ๋ฒ• + * @param maxStackSize ์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ + * @param enableLogging ๋กœ๊น… ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @return ์‚ฌ์šฉ์ž ์ •์˜ LRParser ์ธ์Šคํ„ด์Šค + */ + fun create( + grammar: Grammar = Grammar, + maxStackSize: Int = 1000, + enableLogging: Boolean = false + ): LRParser = LRParser(grammar, maxStackSize, enableLogging) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/LRParserTable.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/LRParserTable.kt new file mode 100644 index 00000000..4a4f6796 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/LRParserTable.kt @@ -0,0 +1,502 @@ +package hs.kr.entrydsm.domain.parser.aggregates + +import hs.kr.entrydsm.global.extensions.* + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.entities.CompressedLRState +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.services.ConflictResolver +import hs.kr.entrydsm.domain.parser.services.ConflictResolutionResult +import hs.kr.entrydsm.domain.parser.services.OptimizedParsingTable +import hs.kr.entrydsm.domain.parser.services.StateCacheManager +import hs.kr.entrydsm.domain.parser.values.FirstFollowSets +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate + +/** + * LR(1) ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ๊ตฌ์ถ•ํ•˜๊ณ  ๊ด€๋ฆฌํ•˜๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค. + * + * ๋ฌธ๋ฒ•์œผ๋กœ๋ถ€ํ„ฐ LR(1) ์ƒํƒœ๋ฅผ ์ž๋™ ์ƒ์„ฑํ•˜๊ณ , LALR ์ตœ์ ํ™”๋ฅผ ์ ์šฉํ•˜๋ฉฐ, + * ์ถฉ๋Œ ํ•ด๊ฒฐ๊ณผ 2D ๋ฐฐ์—ด ์ตœ์ ํ™”๋ฅผ ํ†ตํ•ด ์™„์ „ํ•œ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ LRParserTable์„ DDD ๊ตฌ์กฐ๋กœ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @property productions ๋ฌธ๋ฒ•์˜ ์ƒ์‚ฐ ๊ทœ์น™๋“ค + * @property terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @property nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @property startSymbol ์‹œ์ž‘ ์‹ฌ๋ณผ + * @property augmentedProduction ํ™•์žฅ๋œ ์ƒ์‚ฐ ๊ทœ์น™ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Aggregate(context = "parser") +class LRParserTable private constructor( + private val productions: List, + private val terminals: Set, + private val nonTerminals: Set, + private val startSymbol: TokenType, + private val augmentedProduction: Production, + private val firstFollowSets: FirstFollowSets, + private val conflictResolver: ConflictResolver, + private val stateCache: StateCacheManager +) { + + // ๊ณ„์‚ฐ๋œ ๊ตฌ์„ฑ์š”์†Œ๋“ค (lazy๋กœ ์ดˆ๊ธฐํ™”) + private val states: List> by lazy { buildLRStates() } + private val optimizedTable: OptimizedParsingTable by lazy { buildParsingTable() } + private val conflicts = mutableListOf() + + /** + * LR(1) ์ƒํƒœ๋“ค์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun buildLRStates(): List> { + val states = mutableListOf>() + val stateMap = mutableMapOf, Int>() + + // ์‹œ์ž‘ ์ƒํƒœ ์ƒ์„ฑ + val startItem = LRItem(augmentedProduction, 0, TokenType.DOLLAR) + val startState = closure(setOf(startItem)) + states.add(startState) + stateMap[startState] = 0 + + val workList = mutableListOf(0) + + while (workList.isNotEmpty()) { + val stateId = workList.removeFirst() + val state = states[stateId] + + processStateTransitions(state, states, stateMap, workList) + } + + return states + } + + /** + * ๋‹จ์ผ ์ƒํƒœ์˜ ๋ชจ๋“  ์ „์ด๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ฒ˜๋ฆฌํ•  ์ƒํƒœ + * @param states ์ „์ฒด ์ƒํƒœ ๋ชฉ๋ก + * @param stateMap ์ƒํƒœ ๋งตํ•‘ + * @param workList ์ž‘์—… ๋Œ€๊ธฐ์—ด + */ + private fun processStateTransitions( + state: Set, + states: MutableList>, + stateMap: MutableMap, Int>, + workList: MutableList + ) { + val transitions = computeTransitions(state) + + for ((symbol, itemSet) in transitions) { + val newState = closure(itemSet) + processNewState(newState, states, stateMap, workList) + } + } + + /** + * ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ์ฒ˜๋ฆฌํ•˜๊ณ  ์บ์‹ฑ/๋ณ‘ํ•ฉ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newState ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ์ƒํƒœ + * @param states ์ „์ฒด ์ƒํƒœ ๋ชฉ๋ก + * @param stateMap ์ƒํƒœ ๋งตํ•‘ + * @param workList ์ž‘์—… ๋Œ€๊ธฐ์—ด + */ + private fun processNewState( + newState: Set, + states: MutableList>, + stateMap: MutableMap, Int>, + workList: MutableList + ) { + val cacheResult = stateCache.getOrCacheState(newState, states.size) + + if (cacheResult.isHit) { + return // ์ด๋ฏธ ์กด์žฌํ•˜๋Š” ์ƒํƒœ + } + + val newStateId = addNewState(newState, states, stateMap, workList) + attemptLALRMerge(newState, newStateId, states) + } + + /** + * ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ์ƒํƒœ ๋ชฉ๋ก์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newState ์ถ”๊ฐ€ํ•  ์ƒํƒœ + * @param states ์ „์ฒด ์ƒํƒœ ๋ชฉ๋ก + * @param stateMap ์ƒํƒœ ๋งตํ•‘ + * @param workList ์ž‘์—… ๋Œ€๊ธฐ์—ด + * @return ์ƒˆ ์ƒํƒœ์˜ ID + */ + private fun addNewState( + newState: Set, + states: MutableList>, + stateMap: MutableMap, Int>, + workList: MutableList + ): Int { + val newStateId = states.size + states.add(newState) + stateMap[newState] = newStateId + workList.add(newStateId) + return newStateId + } + + /** + * LALR ๋ณ‘ํ•ฉ์„ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newState ์ƒˆ๋กœ์šด ์ƒํƒœ + * @param newStateId ์ƒˆ ์ƒํƒœ์˜ ID + * @param states ์ „์ฒด ์ƒํƒœ ๋ชฉ๋ก + */ + private fun attemptLALRMerge( + newState: Set, + newStateId: Int, + states: MutableList> + ) { + val compressedState = CompressedLRState.fromItems(newState) + val mergeableStateId = stateCache.findMergeableState(compressedState) + + if (mergeableStateId != null && canMergeStates(states[mergeableStateId], compressedState)) { + performLALRMerge(mergeableStateId, compressedState, newStateId, states) + } + } + + /** + * ๋‘ ์ƒํƒœ๊ฐ€ LALR ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun canMergeStates(existingState: Set, newCompressedState: CompressedLRState): Boolean { + val existingCompressed = CompressedLRState.fromItems(existingState) + return CompressedLRState.canMergeLALR(existingCompressed, newCompressedState) + } + + /** + * LALR ๋ณ‘ํ•ฉ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performLALRMerge( + mergeableStateId: Int, + compressedState: CompressedLRState, + newStateId: Int, + states: MutableList> + ) { + val existingCompressed = CompressedLRState.fromItems(states[mergeableStateId]) + val mergedState = CompressedLRState.mergeLALR(existingCompressed, compressedState) + + states[mergeableStateId] = mergedState.coreItems + states.removeAt(newStateId) + } + + /** + * ์ƒํƒœ์—์„œ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์ „์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun computeTransitions(state: Set): Map> { + val transitions = mutableMapOf>() + + for (item in state) { + val nextSymbol = item.nextSymbol() + if (nextSymbol != null) { + transitions.computeIfAbsent(nextSymbol) { mutableSetOf() } + .add(item.advance()) + } + } + + return transitions + } + + /** + * LR(1) ์•„์ดํ…œ ์ง‘ํ•ฉ์˜ ํด๋กœ์ €๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun closure(items: Set): Set { + val result = items.toMutableSet() + val workList = items.toMutableList() + + while (workList.isNotEmpty()) { + val item = workList.removeFirst() + val nextSymbol = item.nextSymbol() + + if (nextSymbol != null && nextSymbol in nonTerminals) { + val beta = item.beta() + val firstOfBetaLookahead = firstFollowSets.firstOfSequence( + beta + listOf(item.lookahead) + ) + + for (production in productions.filter { it.left == nextSymbol }) { + for (lookahead in firstOfBetaLookahead) { + val newItem = LRItem(production, 0, lookahead) + if (newItem !in result) { + result.add(newItem) + workList.add(newItem) + } + } + } + } + } + + return result + } + + /** + * ์ตœ์ ํ™”๋œ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun buildParsingTable(): OptimizedParsingTable { + val actionMap = mutableMapOf, LRAction>() + val gotoMap = mutableMapOf, Int>() + + // ์•ก์…˜ ํ…Œ์ด๋ธ” ๊ตฌ์ถ• + for ((stateId, state) in states.withIndex()) { + for (item in state) { + if (item.isComplete()) { + // Reduce ์•ก์…˜ + if (item.production.left == TokenType.START && + item.lookahead == TokenType.DOLLAR) { + // Accept ์•ก์…˜ + actionMap[Pair(stateId, TokenType.DOLLAR)] = LRAction.Accept + } else { + // ์ถฉ๋Œ ์ฒ˜๋ฆฌ + val key = Pair(stateId, item.lookahead) + val existing = actionMap[key] + val newAction = LRAction.Reduce(item.production) + + if (existing != null) { + val result = conflictResolver.resolveConflict( + existing, newAction, item.lookahead, stateId + ) + when (result) { + is ConflictResolutionResult.Resolved -> { + actionMap[key] = result.action + } + is ConflictResolutionResult.Unresolved -> { + conflicts.add("Unresolvable conflict in state $stateId: ${result.reason}") + } + } + } else { + actionMap[key] = newAction + } + } + } + } + } + + // GOTO ํ…Œ์ด๋ธ” ๊ตฌ์ถ• (์ „์ด์—์„œ ์ƒ์„ฑ๋จ) + for ((stateId, state) in states.withIndex()) { + val transitions = computeTransitions(state) + for ((symbol, _) in transitions) { + if (symbol in nonTerminals) { + val targetStateId = findTargetState(state, symbol) + if (targetStateId != null) { + gotoMap[Pair(stateId, symbol)] = targetStateId + } + } + } + } + + return OptimizedParsingTable.fromMaps( + actionMap = actionMap, + gotoMap = gotoMap, + terminals = terminals, + nonTerminals = nonTerminals, + numStates = states.size + ) + } + + /** + * ํŠน์ • ์‹ฌ๋ณผ๋กœ ์ „์ดํ•œ ๋ชฉํ‘œ ์ƒํƒœ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + private fun findTargetState(fromState: Set, symbol: TokenType): Int? { + val transitions = computeTransitions(fromState) + val targetItems = transitions[symbol] ?: return null + val targetState = closure(targetItems) + + return states.indexOfFirst { it == targetState }.takeIf { it >= 0 } + } + + /** + * ์ฃผ์–ด์ง„ ์ƒํƒœ์™€ ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ ํŒŒ์‹ฑ ์•ก์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getAction(state: Int, terminal: TokenType): LRAction { + return optimizedTable.getAction(state, terminal) + } + + /** + * ์ฃผ์–ด์ง„ ์ƒํƒœ์™€ ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ GOTO ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getGoto(state: Int, nonTerminal: TokenType): Int? { + return optimizedTable.getGoto(state, nonTerminal) + } + + /** + * ํŒŒ์„œ ํ…Œ์ด๋ธ”์˜ ์ƒํƒœ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getStateCount(): Int { + return states.size + } + + /** + * ๋ฐœ๊ฒฌ๋œ ์ถฉ๋Œ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getConflicts(): List { + return conflicts.toList() + } + + /** + * ํŒŒ์„œ ํ…Œ์ด๋ธ”์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getMemoryStats(): Map { + return mapOf( + "totalStates" to states.size, + "totalProductions" to productions.size, + "tableStats" to optimizedTable.getMemoryStats(), + "cacheStats" to stateCache.getCacheStatistics(), + "conflictCount" to conflicts.size, + "firstFollowStats" to mapOf( + "firstStats" to firstFollowSets.getFirstStats(), + "followStats" to firstFollowSets.getFollowStats() + ) + ) + } + + /** + * ํŒŒ์„œ ํ…Œ์ด๋ธ” ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun generateTableReport(): String { + val sb = StringBuilder() + + sb.appendLine("=== LR(1) ํŒŒ์„œ ํ…Œ์ด๋ธ” ๋ณด๊ณ ์„œ ===") + sb.appendLine("์ƒ์‚ฐ ๊ทœ์น™ ์ˆ˜: ${productions.size}") + sb.appendLine("ํ„ฐ๋ฏธ๋„ ์ˆ˜: ${terminals.size}") + sb.appendLine("๋…ผํ„ฐ๋ฏธ๋„ ์ˆ˜: ${nonTerminals.size}") + sb.appendLine("์ด ์ƒํƒœ ์ˆ˜: ${states.size}") + sb.appendLine("์ถฉ๋Œ ์ˆ˜: ${conflicts.size}") + sb.appendLine() + + val memStats = getMemoryStats() + @Suppress("UNCHECKED_CAST") + val tableStats = memStats["tableStats"] as Map + sb.appendLine("=== ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ===") + sb.appendLine("์ถ”์ • ๋ฉ”๋ชจ๋ฆฌ: ${tableStats["estimatedMemoryBytes"]} bytes") + sb.appendLine("์•ก์…˜ ํ…Œ์ด๋ธ” ๋ฐ€๋„: ${String.format("%.2f%%", (tableStats["actionDensity"] as Double) * 100)}") + sb.appendLine("GOTO ํ…Œ์ด๋ธ” ๋ฐ€๋„: ${String.format("%.2f%%", (tableStats["gotoDensity"] as Double) * 100)}") + sb.appendLine() + + @Suppress("UNCHECKED_CAST") + val cacheStats = memStats["cacheStats"] as Map + sb.appendLine("=== ์ƒํƒœ ์บ์‹ฑ ํšจ์œจ์„ฑ ===") + sb.appendLine("์บ์‹œ ํžˆํŠธ์œจ: ${String.format("%.2f%%", (cacheStats["hitRate"] as Double) * 100)}") + sb.appendLine("์บ์‹œ ํšจ์œจ์„ฑ: ${String.format("%.2f%%", (cacheStats["cacheEfficiency"] as Double) * 100)}") + + if (conflicts.isNotEmpty()) { + sb.appendLine() + sb.appendLine("=== ๋ฐœ๊ฒฌ๋œ ์ถฉ๋Œ๋“ค ===") + conflicts.take(10).forEach { conflict -> + sb.appendLine(" $conflict") + } + if (conflicts.size > 10) { + sb.appendLine(" ... ๊ทธ ์™ธ ${conflicts.size - 10}๊ฐœ ์ถฉ๋Œ") + } + } + + return sb.toString() + } + + companion object { + /** + * ๋ฌธ๋ฒ• ์ •๋ณด๋กœ๋ถ€ํ„ฐ LR ํŒŒ์„œ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun create( + productions: List, + terminals: Set, + nonTerminals: Set, + startSymbol: TokenType + ): LRParserTable { + if (productions.isEmpty()) { + throw ParserException.emptyProductions() + } + + if (terminals.isEmpty()) { + throw ParserException.emptyTerminals() + } + + if (nonTerminals.isEmpty()) { + throw ParserException.emptyNonTerminals() + } + + if (startSymbol !in nonTerminals) { + throw ParserException.invalidStartSymbol(startSymbol) + } + + + // ํ™•์žฅ๋œ ์ƒ์‚ฐ ๊ทœ์น™ ์ƒ์„ฑ + val augmentedProduction = Production( + id = -1, + left = TokenType.START, + right = listOf(startSymbol, TokenType.DOLLAR) + ) + + val extendedNonTerminals = nonTerminals + TokenType.START + + // ์˜์กด์„ฑ ์ƒ์„ฑ + val firstFollowSets = FirstFollowSets.compute( + productions = productions, + terminals = terminals, + nonTerminals = extendedNonTerminals, + startSymbol = startSymbol + ) + + val conflictResolver = ConflictResolver.create() + val stateCache = StateCacheManager.create() + + return LRParserTable( + productions = productions, + terminals = terminals, + nonTerminals = extendedNonTerminals, + startSymbol = startSymbol, + augmentedProduction = augmentedProduction, + firstFollowSets = firstFollowSets, + conflictResolver = conflictResolver, + stateCache = stateCache + ) + } + + /** + * POC ์ฝ”๋“œ์™€ ํ˜ธํ™˜๋˜๋Š” ๊ธฐ๋ณธ ๋ฌธ๋ฒ•์œผ๋กœ ํŒŒ์„œ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createWithDefaultGrammar(): LRParserTable { + // POC ์ฝ”๋“œ์˜ ๊ธฐ๋ณธ ๋ฌธ๋ฒ• ์ •์˜ + val terminals = setOf( + TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.VARIABLE, + TokenType.PLUS, TokenType.MINUS, TokenType.MULTIPLY, TokenType.DIVIDE, + TokenType.POWER, TokenType.MODULO, + TokenType.EQUAL, TokenType.NOT_EQUAL, TokenType.LESS, TokenType.LESS_EQUAL, + TokenType.GREATER, TokenType.GREATER_EQUAL, + TokenType.AND, TokenType.OR, TokenType.NOT, + TokenType.LEFT_PAREN, TokenType.RIGHT_PAREN, TokenType.COMMA, + TokenType.IF, TokenType.TRUE, TokenType.FALSE, TokenType.DOLLAR + ) + + val nonTerminals = setOf( + TokenType.EXPR, TokenType.AND_EXPR, TokenType.COMP_EXPR, + TokenType.ARITH_EXPR, TokenType.TERM, TokenType.FACTOR, + TokenType.PRIMARY, TokenType.ARGS + ) + + val productions = createDefaultProductions() + + return create( + productions = productions, + terminals = terminals, + nonTerminals = nonTerminals, + startSymbol = TokenType.EXPR + ) + } + + /** + * POC ์ฝ”๋“œ์˜ 34๊ฐœ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private fun createDefaultProductions(): List { + // ์—ฌ๊ธฐ์— POC ์ฝ”๋“œ์˜ 34๊ฐœ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ •์˜ + // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” Grammar ๊ฐ์ฒด์—์„œ ๊ฐ€์ ธ์˜ค๊ฑฐ๋‚˜ ๋ณ„๋„๋กœ ์ •์˜ + return emptyList() // ์ž„์‹œ ๊ตฌํ˜„ + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/ParsingContextAggregate.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/ParsingContextAggregate.kt new file mode 100644 index 00000000..0efead94 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/aggregates/ParsingContextAggregate.kt @@ -0,0 +1,655 @@ +package hs.kr.entrydsm.domain.parser.aggregates + +import hs.kr.entrydsm.global.extensions.* + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.ParsingState +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.domain.parser.interfaces.ParserContract +import hs.kr.entrydsm.domain.parser.services.ParserService +import hs.kr.entrydsm.domain.parser.values.Grammar +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.parser.values.ParsingResult +import hs.kr.entrydsm.domain.parser.values.ParsingTable +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate + +/** + * ํŒŒ์‹ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค. + * + * DDD Aggregate ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํŒŒ์‹ฑ ๊ณผ์ •์˜ ์ „์ฒด ์ปจํ…์ŠคํŠธ์™€ ์ƒํƒœ๋ฅผ + * ์ผ๊ด€์„ฑ ์žˆ๊ฒŒ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์‹ฑ ์Šคํƒ, ํ˜„์žฌ ์ƒํƒœ, ์ž…๋ ฅ ๋ฒ„ํผ, ์—๋Ÿฌ ๋ณต๊ตฌ ๋“ฑ + * ํŒŒ์‹ฑ๊ณผ ๊ด€๋ จ๋œ ๋ชจ๋“  ์ •๋ณด๋ฅผ ํ†ตํ•ฉ์ ์œผ๋กœ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @property grammar ์‚ฌ์šฉํ•  ๋ฌธ๋ฒ• + * @property parsingTable ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” + * @property parserService ํŒŒ์„œ ์„œ๋น„์Šค + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Aggregate(context = "parser") +class ParsingContextAggregate( + private val grammar: Grammar, + private val parsingTable: ParsingTable, + private val parserService: ParserService +) : ParserContract { + + companion object { + private const val MAX_STACK_SIZE = 10000 + private const val MAX_ERROR_RECOVERY_ATTEMPTS = 100 + private const val MAX_PARSING_STEPS = 100000 + } + + // ํŒŒ์‹ฑ ์ƒํƒœ + private val stateStack = mutableListOf() + private val astStack = mutableListOf() + private var currentState: Int = parsingTable.startState + private var inputTokens = mutableListOf() + private var currentTokenIndex = 0 + + // ํŒŒ์‹ฑ ํ†ต๊ณ„ + private var stepCount = 0 + private var shiftCount = 0 + private var reduceCount = 0 + private var errorRecoveryCount = 0 + + // ์„ค์ • + private var debugMode = false + private var errorRecoveryMode = true + private var maxParsingDepth = MAX_STACK_SIZE + + // ํŒŒ์‹ฑ ๊ธฐ๋ก + private val parsingTrace = mutableListOf() + private val errorHistory = mutableListOf() + + /** + * ํŒŒ์‹ฑ ๋‹จ๊ณ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class ParsingStep( + val stepNumber: Int, + val action: String, + val currentState: Int, + val currentToken: TokenType?, + val stackSize: Int, + val timestamp: Long = System.currentTimeMillis() + ) + + /** + * ํŒŒ์‹ฑ ์—๋Ÿฌ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class ParsingError( + val errorType: String, + val message: String, + val tokenIndex: Int, + val stackState: List, + val recoveryAction: String?, + val timestamp: Long = System.currentTimeMillis() + ) + + /** + * ํ† ํฐ ๋ชฉ๋ก์„ ๊ตฌ๋ฌธ ๋ถ„์„ํ•˜์—ฌ AST๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ตฌ๋ฌธ ๋ถ„์„ํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ (AST ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ) + */ + override fun parse(tokens: List): ParsingResult { + val startTime = System.currentTimeMillis() + + try { + initializeParsing(tokens) + val result = performParsing() + val duration = System.currentTimeMillis() - startTime + + return result.copy( + duration = duration, + metadata = result.metadata + getParsingMetadata() + ) + + } catch (e: Exception) { + val duration = System.currentTimeMillis() - startTime + recordError("ParsingException", e.message ?: "Unknown error", null) + + return ParsingResult.failure( + error = ParserException( + errorCode = hs.kr.entrydsm.global.exception.ErrorCode.LR_PARSING_ERROR, + message = "ํŒŒ์‹ฑ ์‹คํŒจ: ${e.message}", + cause = e + ), + duration = duration, + tokenCount = tokens.size, + metadata = getParsingMetadata() + ) + } finally { + if (debugMode) { + printParsingTrace() + } + } + } + + /** + * ๋‹จ์ผ ํ† ํฐ ์ŠคํŠธ๋ฆผ์„ ๊ตฌ๋ฌธ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenSequence ํ† ํฐ ์‹œํ€€์Šค + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + override fun parseSequence(tokenSequence: Sequence): ParsingResult { + return parse(tokenSequence.toList()) + } + + /** + * ์ฃผ์–ด์ง„ ํ† ํฐ ๋ชฉ๋ก์ด ๋ฌธ๋ฒ•์ ์œผ๋กœ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ์œ ํšจํ•˜๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + */ + override fun validate(tokens: List): Boolean { + return try { + val result = parse(tokens) + result.isSuccess + } catch (e: Exception) { + false + } + } + + /** + * ๋ถ€๋ถ„ ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ตฌ๋ฌธ ์™„์„ฑ, ์—๋Ÿฌ ๋ณต๊ตฌ ๋“ฑ์— ์‚ฌ์šฉ). + * + * @param tokens ๋ถ€๋ถ„ ํ† ํฐ ๋ชฉ๋ก + * @param allowIncomplete ๋ถˆ์™„์ „ํ•œ ๊ตฌ๋ฌธ ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @return ๋ถ€๋ถ„ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + override fun parsePartial(tokens: List, allowIncomplete: Boolean): ParsingResult { + val originalErrorRecovery = errorRecoveryMode + + try { + errorRecoveryMode = true + val result = parse(tokens) + + // ๋ถˆ์™„์ „ํ•œ ํŒŒ์‹ฑ๋„ ํ—ˆ์šฉํ•˜๋Š” ๊ฒฝ์šฐ ๋ถ€๋ถ„ ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜ + if (allowIncomplete && result.isFailure() && astStack.isNotEmpty()) { + return ParsingResult.success( + ast = astStack.lastOrNull() ?: createEmptyAST(), + warnings = listOf("๋ถ€๋ถ„ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค"), + tokenCount = tokens.size, + metadata = getParsingMetadata() + ("partialParsing" to true) + ) + } + + return result + + } finally { + errorRecoveryMode = originalErrorRecovery + } + } + + /** + * ๋‹ค์Œ์— ์˜ฌ ์ˆ˜ ์žˆ๋Š” ์œ ํšจํ•œ ํ† ํฐ๋“ค์„ ์˜ˆ์ธกํ•ฉ๋‹ˆ๋‹ค. + * + * @param currentTokens ํ˜„์žฌ๊นŒ์ง€์˜ ํ† ํฐ ๋ชฉ๋ก + * @return ๋‹ค์Œ์— ์˜ฌ ์ˆ˜ ์žˆ๋Š” ํ† ํฐ ํƒ€์ž…๋“ค + */ + override fun predictNextTokens(currentTokens: List): Set { + // ํ˜„์žฌ ์ƒํƒœ์—์„œ ๊ฐ€๋Šฅํ•œ ์•ก์…˜๋“ค์˜ ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค ๋ฐ˜ํ™˜ + val currentParsingState = parsingTable.getState(currentState) + return currentParsingState.actions.keys.toSet() + } + + /** + * ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์œ„์น˜์™€ ์˜ˆ์ƒ ํ† ํฐ์„ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๋ถ„์„ํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ์˜ค๋ฅ˜ ๋ถ„์„ ๊ฒฐ๊ณผ + */ + override fun analyzeErrors(tokens: List): Map { + val result = parse(tokens) + + return mapOf( + "hasErrors" to result.isFailure(), + "errorHistory" to errorHistory, + "expectedTokens" to predictNextTokens(tokens), + "currentPosition" to currentTokenIndex, + "stackDepth" to stateStack.size, + "parsingSteps" to stepCount, + "errorRecoveryAttempts" to errorRecoveryCount + ) + } + + /** + * ํŒŒ์„œ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์„œ ์ƒํƒœ ์ •๋ณด + */ + override fun getState(): Map = mapOf( + "currentState" to currentState, + "stackSize" to stateStack.size, + "currentTokenIndex" to currentTokenIndex, + "inputTokensRemaining" to (inputTokens.size - currentTokenIndex), + "stepCount" to stepCount, + "shiftCount" to shiftCount, + "reduceCount" to reduceCount, + "errorRecoveryCount" to errorRecoveryCount, + "debugMode" to debugMode, + "errorRecoveryMode" to errorRecoveryMode, + "maxParsingDepth" to maxParsingDepth + ) + + /** + * ํŒŒ์„œ๋ฅผ ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun reset() { + stateStack.clear() + astStack.clear() + currentState = parsingTable.startState + inputTokens.clear() + currentTokenIndex = 0 + stepCount = 0 + shiftCount = 0 + reduceCount = 0 + errorRecoveryCount = 0 + parsingTrace.clear() + errorHistory.clear() + debugMode = false + errorRecoveryMode = true + maxParsingDepth = MAX_STACK_SIZE + } + + /** + * ํŒŒ์„œ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + override fun getConfiguration(): Map = mapOf( + "maxStackSize" to MAX_STACK_SIZE, + "maxErrorRecoveryAttempts" to MAX_ERROR_RECOVERY_ATTEMPTS, + "maxParsingSteps" to MAX_PARSING_STEPS, + "grammarInfo" to grammar.getGrammarStatistics(), + "parsingTableSize" to parsingTable.getSizeInfo() + ) + + /** + * ํŒŒ์‹ฑ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต (ํŒŒ์‹ฑ ํšŸ์ˆ˜, ์„ฑ๊ณต๋ฅ , ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๋“ฑ) + */ + override fun getStatistics(): Map = mapOf( + "aggregateName" to "ParsingContextAggregate", + "currentSessionStats" to mapOf( + "stepCount" to stepCount, + "shiftCount" to shiftCount, + "reduceCount" to reduceCount, + "errorRecoveryCount" to errorRecoveryCount, + "maxStackDepth" to stateStack.size, + "parsingTraceSize" to parsingTrace.size, + "errorHistorySize" to errorHistory.size + ) + ) + + /** + * ๋””๋ฒ„๊ทธ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ๋””๋ฒ„๊ทธ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + override fun setDebugMode(enabled: Boolean) { + debugMode = enabled + } + + /** + * ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + override fun setErrorRecoveryMode(enabled: Boolean) { + errorRecoveryMode = enabled + } + + /** + * ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxDepth ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด + */ + override fun setMaxParsingDepth(maxDepth: Int) { + if (maxDepth <= 0) { + throw ParserException.maxDepthNonPositive(maxDepth) + } + + if (maxDepth > MAX_STACK_SIZE) { + throw ParserException.maxDepthExceedsLimit(maxDepth, MAX_STACK_SIZE) + } + + this.maxParsingDepth = maxDepth + } + + /** + * ์ŠคํŠธ๋ฆฌ๋ฐ ๋ชจ๋“œ๋กœ ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ํ† ํฐ ๋ชฉ๋ก + * @param callback ํŒŒ์‹ฑ ์ง„ํ–‰ ์ƒํ™ฉ ์ฝœ๋ฐฑ + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + override fun parseStreaming(tokens: List, callback: (progress: Double) -> Unit): ParsingResult { + return parserService.parseStreaming(tokens, callback) + } + + /** + * ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ตฌ๋ฌธ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๋ถ„์„ํ•  ํ† ํฐ ๋ชฉ๋ก + * @param callback ๋ถ„์„ ์™„๋ฃŒ ์‹œ ํ˜ธ์ถœ๋  ์ฝœ๋ฐฑ ํ•จ์ˆ˜ + */ + override fun parseAsync(tokens: List, callback: (ParsingResult) -> Unit) { + parserService.parseAsync(tokens, callback) + } + + /** + * ์ฆ๋ถ„ ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * ๊ธฐ์กด ํŒŒ์‹ฑ ๊ฒฐ๊ณผ๋ฅผ ์žฌํ™œ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @param previousResult ์ด์ „ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + * @param newTokens ์ƒˆ๋กœ์šด ํ† ํฐ ๋ชฉ๋ก + * @param changeStartIndex ๋ณ€๊ฒฝ ์‹œ์ž‘ ์œ„์น˜ + * @return ์ฆ๋ถ„ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + override fun incrementalParse( + previousResult: ParsingResult, + newTokens: List, + changeStartIndex: Int + ): ParsingResult { + return parserService.incrementalParse(previousResult, newTokens, changeStartIndex) + } + + /** + * ๋ฌธ๋ฒ• ๊ทœ์น™์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฌธ๋ฒ•์ด ์œ ํšจํ•˜๋ฉด true + */ + override fun validateGrammar(): Boolean { + return grammar.isValid() && parsingTable.isLR1Valid() + } + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ์ถฉ๋Œ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ถฉ๋Œ ์ •๋ณด ๋งต + */ + override fun checkParsingConflicts(): Map { + return parsingTable.getConflicts().let { conflicts -> + mapOf( + "hasConflicts" to conflicts.isNotEmpty(), + "conflictTypes" to conflicts.keys, + "conflictDetails" to conflicts + ) + } + } + + /** + * ํŠน์ • ์œ„์น˜์—์„œ์˜ ํŒŒ์‹ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenIndex ํ† ํฐ ์ธ๋ฑ์Šค + * @return ํŒŒ์‹ฑ ์ปจํ…์ŠคํŠธ ์ •๋ณด + */ + override fun getParsingContext(tokenIndex: Int): Map { + return mapOf( + "tokenIndex" to tokenIndex, + "currentState" to currentState, + "stackStates" to stateStack.toList(), + "availableActions" to getCurrentAvailableActions(), + "expectedTokens" to predictNextTokens(inputTokens.take(tokenIndex)), + "parsingSteps" to parsingTrace.filter { it.stepNumber <= tokenIndex } + ) + } + + /** + * ํ˜„์žฌ ํŒŒ์‹ฑ ์Šคํƒ์˜ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์‹ฑ ์Šคํƒ ์ •๋ณด + */ + override fun getParsingStack(): List { + return stateStack.mapIndexed { index, stateId -> + mapOf( + "index" to index, + "stateId" to stateId, + "astNode" to (astStack.getOrNull(index)?.toString() ?: "null") + ) + } + } + + /** + * ํŒŒ์„œ๊ฐ€ ์ง€์›ํ•˜๋Š” ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜ + */ + override fun getMaxSupportedTokens(): Int = 50000 + + /** + * ํŒŒ์„œ์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ •๋ณด + */ + override fun getMemoryUsage(): Map { + return mapOf( + "stateStackSize" to (stateStack.size * 4), // Int ํฌ๊ธฐ + "astStackSize" to (astStack.size * 100), // ๋Œ€๋žต์  AST ๋…ธ๋“œ ํฌ๊ธฐ + "inputTokensSize" to (inputTokens.size * 50), // ๋Œ€๋žต์  ํ† ํฐ ํฌ๊ธฐ + "parsingTraceSize" to (parsingTrace.size * 100), // ๋Œ€๋žต์  trace ํฌ๊ธฐ + "errorHistorySize" to (errorHistory.size * 200), // ๋Œ€๋žต์  error ํฌ๊ธฐ + "totalEstimatedSize" to calculateTotalMemoryUsage() + ) + } + + // Private helper methods + + private fun initializeParsing(tokens: List) { + reset() + inputTokens.addAll(tokens) + stateStack.add(currentState) + + if (debugMode) { + recordStep("INIT", "ํŒŒ์‹ฑ ์‹œ์ž‘") + } + } + + private fun performParsing(): ParsingResult { + while (stepCount < MAX_PARSING_STEPS && currentTokenIndex <= inputTokens.size) { + if (stateStack.size > maxParsingDepth) { + throw IllegalStateException("์Šคํƒ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: ${stateStack.size} > $maxParsingDepth") + } + + val currentToken = getCurrentToken() + val action = getActionForCurrentState(currentToken?.type ?: TokenType.DOLLAR) + + when { + action == null -> { + if (errorRecoveryMode) { + performErrorRecovery(currentToken) + } else { + throw IllegalStateException("์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ํ† ํฐ: ${currentToken?.type}") + } + } + action is LRAction.Shift -> performShift(action, currentToken!!) + action is LRAction.Reduce -> performReduce(action) + action is LRAction.Accept -> return createSuccessResult() + else -> throw IllegalStateException("์•Œ ์ˆ˜ ์—†๋Š” ์•ก์…˜: $action") + } + + stepCount++ + } + + throw IllegalStateException("ํŒŒ์‹ฑ์ด ์ตœ๋Œ€ ๋‹จ๊ณ„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค") + } + + private fun getCurrentToken(): Token? { + return if (currentTokenIndex < inputTokens.size) { + inputTokens[currentTokenIndex] + } else { + null + } + } + + private fun getActionForCurrentState(tokenType: TokenType): LRAction? { + return parsingTable.getAction(currentState, tokenType) + } + + private fun performShift(action: LRAction.Shift, token: Token) { + currentState = action.state + stateStack.add(currentState) + astStack.add(createLeafAST(token)) + currentTokenIndex++ + shiftCount++ + + if (debugMode) { + recordStep("SHIFT", "Shift to state $currentState, token: ${token.type}") + } + } + + private fun performReduce(action: LRAction.Reduce) { + val production = action.production + val rightSize = production.right.size + + // ์Šคํƒ์—์„œ ์‹ฌ๋ณผ๋“ค ์ œ๊ฑฐ + val astChildren = mutableListOf() + repeat(rightSize) { + if (stateStack.isNotEmpty()) stateStack.removeLastOrNull() + astChildren.add(0, astStack.removeLastOrNull()) + } + + // AST ๋…ธ๋“œ ์ƒ์„ฑ + val newAST = production.astBuilder.build(astChildren.filterNotNull()) + astStack.add(newAST as? ASTNode) + + // Goto ์ˆ˜ํ–‰ + val gotoState = parsingTable.getGoto(stateStack.lastOrNull() ?: 0, production.left) + if (gotoState != null) { + currentState = gotoState + stateStack.add(currentState) + } else { + throw IllegalStateException("Goto ์ƒํƒœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: ${production.left}") + } + + reduceCount++ + + if (debugMode) { + recordStep("REDUCE", "Reduce by production ${production.id}: ${production.left} -> ${production.right.joinToString(" ")}") + } + } + + private fun performErrorRecovery(currentToken: Token?) { + errorRecoveryCount++ + + if (errorRecoveryCount > MAX_ERROR_RECOVERY_ATTEMPTS) { + throw IllegalStateException("์—๋Ÿฌ ๋ณต๊ตฌ ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค") + } + + // ๊ฐ„๋‹จํ•œ ์—๋Ÿฌ ๋ณต๊ตฌ: ํ˜„์žฌ ํ† ํฐ ์Šคํ‚ต + if (currentToken != null) { + recordError("UnexpectedToken", "์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ํ† ํฐ: ${currentToken.type}", "TokenSkip") + currentTokenIndex++ + } else { + recordError("UnexpectedEOF", "์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ์ž…๋ ฅ ์ข…๋ฃŒ", "EOFHandling") + } + + if (debugMode) { + recordStep("ERROR_RECOVERY", "์—๋Ÿฌ ๋ณต๊ตฌ ์ˆ˜ํ–‰") + } + } + + private fun createSuccessResult(): ParsingResult { + val finalAST = astStack.lastOrNull() ?: createEmptyAST() + + return ParsingResult.success( + ast = finalAST, + tokenCount = inputTokens.size, + nodeCount = calculateNodeCount(finalAST), + maxDepth = calculateMaxDepth(finalAST), + metadata = getParsingMetadata() + ) + } + + private fun createLeafAST(token: Token): ASTNode { + return hs.kr.entrydsm.domain.ast.entities.VariableNode(token.value) + } + + private fun createEmptyAST(): ASTNode { + return hs.kr.entrydsm.domain.ast.entities.NumberNode(0.0) + } + + private fun recordStep(action: String, description: String) { + parsingTrace.add( + ParsingStep( + stepNumber = stepCount, + action = action, + currentState = currentState, + currentToken = getCurrentToken()?.type, + stackSize = stateStack.size + ) + ) + } + + private fun recordError(errorType: String, message: String, recoveryAction: String?) { + errorHistory.add( + ParsingError( + errorType = errorType, + message = message, + tokenIndex = currentTokenIndex, + stackState = stateStack.toList(), + recoveryAction = recoveryAction + ) + ) + } + + private fun getCurrentAvailableActions(): List { + val currentParsingState = parsingTable.getState(currentState) + return currentParsingState.actions.map { (terminal, action) -> + "$terminal: ${action.getActionType()}" + } + } + + private fun getParsingMetadata(): Map = mapOf( + "stepCount" to stepCount, + "shiftCount" to shiftCount, + "reduceCount" to reduceCount, + "errorRecoveryCount" to errorRecoveryCount, + "maxStackDepth" to (stateStack.maxOrNull() ?: 0), + "finalStackSize" to stateStack.size, + "parsingTraceSize" to parsingTrace.size, + "errorHistorySize" to errorHistory.size + ) + + private fun calculateNodeCount(ast: ASTNode): Int { + return 1 + ast.getChildren().sumOf { calculateNodeCount(it) } + } + + private fun calculateMaxDepth(ast: ASTNode): Int { + return if (ast.getChildren().isEmpty()) { + 1 + } else { + 1 + (ast.getChildren().maxOfOrNull { calculateMaxDepth(it) } ?: 0) + } + } + + private fun calculateTotalMemoryUsage(): Long { + return (stateStack.size * 4 + + astStack.size * 100 + + inputTokens.size * 50 + + parsingTrace.size * 100 + + errorHistory.size * 200).toLong() + } + + private fun printParsingTrace() { + println("=== ํŒŒ์‹ฑ ์ถ”์  ์ •๋ณด ===") + parsingTrace.forEach { step -> + println("Step ${step.stepNumber}: ${step.action} - State: ${step.currentState}, Token: ${step.currentToken}, Stack: ${step.stackSize}") + } + + if (errorHistory.isNotEmpty()) { + println("=== ์—๋Ÿฌ ๊ธฐ๋ก ===") + errorHistory.forEach { error -> + println("${error.errorType}: ${error.message} at token ${error.tokenIndex}") + } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/CompressedLRState.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/CompressedLRState.kt new file mode 100644 index 00000000..acd0c4a1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/CompressedLRState.kt @@ -0,0 +1,288 @@ +package hs.kr.entrydsm.domain.parser.entities + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ์„ ์œ„ํ•ด ์••์ถ•๋œ LR ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. + * + * LR(1) ํŒŒ์„œ์˜ ์ƒํƒœ๋Š” ๋งŽ์€ ๋ฉ”๋ชจ๋ฆฌ๋ฅผ ์†Œ๋น„ํ•  ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ, + * ํ•ต์‹ฌ ์ •๋ณด๋งŒ์„ ์ €์žฅํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ค„์ด๊ณ  ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * LALR ์ƒํƒœ ๋ณ‘ํ•ฉ๊ณผ ์ƒํƒœ ์บ์‹ฑ ์‹œ์Šคํ…œ์˜ ๊ธฐ๋ฐ˜์ด ๋ฉ๋‹ˆ๋‹ค. + * + * @property coreItems ํ•ต์‹ฌ ์•„์ดํ…œ๋“ค (lookahead ์ œ์™ธํ•œ core ์ •๋ณด) + * @property isBuilt ์™„์ „ํžˆ ๊ตฌ์ถ•๋˜์—ˆ๋Š”์ง€ ์—ฌ๋ถ€ + * @property signature ์ƒํƒœ์˜ ๊ณ ์œ  ์‹๋ณ„์ž + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Entity(context = "parser", aggregateRoot = CompressedLRState::class) +data class CompressedLRState( + val coreItems: Set, + val isBuilt: Boolean = false, + val signature: String = generateSignature(coreItems) +) { + + init { + if (coreItems.isEmpty()) { + throw ParserException.emptyCoreItems() + } + } + + /** + * ํ•ต์‹ฌ ์•„์ดํ…œ์˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•ต์‹ฌ ์•„์ดํ…œ ๊ฐœ์ˆ˜ + */ + fun getCoreItemCount(): Int = coreItems.size + + /** + * ์ƒํƒœ๊ฐ€ ์™„์ „ํžˆ ๊ตฌ์ถ•๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ตฌ์ถ• ์™„๋ฃŒ ์—ฌ๋ถ€ + */ + fun isFullyBuilt(): Boolean = isBuilt + + /** + * ๋‹ค๋ฅธ ์••์ถ•๋œ ์ƒํƒœ์™€ ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๋‹ค๋ฅธ ์••์ถ•๋œ ์ƒํƒœ + * @return core๊ฐ€ ๋™์ผํ•˜๋ฉด true + */ + fun hasSameCore(other: CompressedLRState): Boolean { + return signature == other.signature + } + + /** + * ์ด ์ƒํƒœ๋ฅผ ์™„์ „ํžˆ ๊ตฌ์ถ•๋œ ์ƒํƒœ๋กœ ๋งˆํ‚นํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ตฌ์ถ• ์™„๋ฃŒ๋กœ ๋งˆํ‚น๋œ ์ƒˆ๋กœ์šด CompressedLRState + */ + fun markAsBuilt(): CompressedLRState { + return copy(isBuilt = true) + } + + /** + * ํ•ต์‹ฌ ์•„์ดํ…œ์— ํฌํ•จ๋œ ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™ ID๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์‚ฐ ๊ทœ์น™ ID ์ง‘ํ•ฉ + */ + fun getProductionIds(): Set { + return coreItems.map { it.production.id }.toSet() + } + + /** + * ํŠน์ • dot ์œ„์น˜๋ฅผ ๊ฐ€์ง„ ์•„์ดํ…œ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param dotPos ๊ฒ€์ƒ‰ํ•  dot ์œ„์น˜ + * @return ํ•ด๋‹น dot ์œ„์น˜๋ฅผ ๊ฐ€์ง„ ์•„์ดํ…œ๋“ค + */ + fun getItemsWithDotPosition(dotPos: Int): Set { + return coreItems.filter { it.dotPos == dotPos }.toSet() + } + + /** + * ์™„๋ฃŒ๋œ ์•„์ดํ…œ๋“ค (dot์ด ๋์— ์žˆ๋Š” ์•„์ดํ…œ๋“ค)์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์™„๋ฃŒ๋œ ์•„์ดํ…œ๋“ค + */ + fun getCompleteItems(): Set { + return coreItems.filter { it.isComplete() }.toSet() + } + + /** + * ํŠน์ • ์‹ฌ๋ณผ๋กœ ์‹œํ”„ํŠธ ๊ฐ€๋Šฅํ•œ ์•„์ดํ…œ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ์‹œํ”„ํŠธํ•  ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ์‹ฌ๋ณผ๋กœ ์‹œํ”„ํŠธ ๊ฐ€๋Šฅํ•œ ์•„์ดํ…œ๋“ค + */ + fun getShiftableItems(symbol: Any): Set { + return coreItems.filter { it.nextSymbol() == symbol }.toSet() + } + + /** + * ์ƒํƒœ์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ •๋ณด ๋งต + */ + fun getMemoryStats(): Map { + return mapOf( + "coreItemCount" to coreItems.size, + "signatureLength" to signature.length, + "isBuilt" to isBuilt, + "uniqueProductionCount" to getProductionIds().size, + "completeItemCount" to getCompleteItems().size + ) + } + + companion object { + /** + * LR ์•„์ดํ…œ ์ง‘ํ•ฉ์œผ๋กœ๋ถ€ํ„ฐ ํ•ต์‹ฌ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * lookahead๋ฅผ ์ œ์™ธํ•œ core ์ •๋ณด๋งŒ ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์ ์ธ ์‹๋ณ„์ž๋ฅผ ๋งŒ๋“ญ๋‹ˆ๋‹ค. + * + * @param coreItems ํ•ต์‹ฌ ์•„์ดํ…œ๋“ค + * @return ์ƒ์„ฑ๋œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ + */ + private fun generateSignature(coreItems: Set): String { + return coreItems + .map { "${it.production.id}:${it.dotPos}" } + .sorted() + .joinToString("|") + } + + /** + * LR ์•„์ดํ…œ ์ง‘ํ•ฉ์œผ๋กœ๋ถ€ํ„ฐ CompressedLRState๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param items LR ์•„์ดํ…œ ์ง‘ํ•ฉ + * @param isBuilt ๊ตฌ์ถ• ์™„๋ฃŒ ์—ฌ๋ถ€ + * @return ์ƒ์„ฑ๋œ CompressedLRState + */ + fun fromItems(items: Set, isBuilt: Boolean = false): CompressedLRState { + if (items.isEmpty()) { + throw ParserException.emptyItems() + } + + return CompressedLRState( + coreItems = items, + isBuilt = isBuilt + ) + } + + /** + * ๋‘ ์••์ถ•๋œ ์ƒํƒœ๊ฐ€ LALR ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ง€๊ณ  ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์œผ๋ฉด ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param state1 ์ฒซ ๋ฒˆ์งธ ์ƒํƒœ + * @param state2 ๋‘ ๋ฒˆ์งธ ์ƒํƒœ + * @return ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun canMergeLALR(state1: CompressedLRState, state2: CompressedLRState): Boolean { + if (!state1.hasSameCore(state2)) { + return false + } + + return !hasLookaheadConflicts(state1.coreItems, state2.coreItems) + } + + /** + * ๋‘ ์•„์ดํ…œ ์ง‘ํ•ฉ ๊ฐ„์˜ lookahead ์ถฉ๋Œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * ์ตœ์ ํ™”๋œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์œผ๋กœ ์กฐ๊ธฐ ์ข…๋ฃŒ๊ฐ€ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param items1 ์ฒซ ๋ฒˆ์งธ ์•„์ดํ…œ ์ง‘ํ•ฉ + * @param items2 ๋‘ ๋ฒˆ์งธ ์•„์ดํ…œ ์ง‘ํ•ฉ + * @return ์ถฉ๋Œ์ด ์žˆ์œผ๋ฉด true + */ + private fun hasLookaheadConflicts(items1: Set, items2: Set): Boolean { + // ๋” ์ž‘์€ ์ง‘ํ•ฉ์„ ์™ธ๋ถ€ ๋ฃจํ”„๋กœ ์‚ฌ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™” + val (smaller, larger) = if (items1.size <= items2.size) { + items1 to items2 + } else { + items2 to items1 + } + + for (item1 in smaller) { + if (hasConflictingLookahead(item1, larger)) { + return true // ์ฒซ ์ถฉ๋Œ ๋ฐœ๊ฒฌ ์‹œ ์ฆ‰์‹œ ์ข…๋ฃŒ + } + } + return false + } + + /** + * ์ฃผ์–ด์ง„ ์•„์ดํ…œ์ด ์•„์ดํ…œ ์ง‘ํ•ฉ๊ณผ lookahead ์ถฉ๋Œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param targetItem ํ™•์ธํ•  ์•„์ดํ…œ + * @param itemSet ๋น„๊ตํ•  ์•„์ดํ…œ ์ง‘ํ•ฉ + * @return ์ถฉ๋Œ์ด ์žˆ์œผ๋ฉด true + */ + private fun hasConflictingLookahead(targetItem: LRItem, itemSet: Set): Boolean { + val targetCore = getCoreKey(targetItem) + + return itemSet.any { item -> + getCoreKey(item) == targetCore && item.lookahead == targetItem.lookahead + } + } + + /** + * ์•„์ดํ…œ์˜ core key๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * Core๋Š” production๊ณผ dot position์œผ๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. + * + * @param item LR ์•„์ดํ…œ + * @return core key ๋ฌธ์ž์—ด + */ + private fun getCoreKey(item: LRItem): String { + return "${item.production.id}:${item.dotPos}" + } + + /** + * ๋‘ LALR ์ƒํƒœ๋ฅผ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ง„ ์•„์ดํ…œ๋“ค์˜ lookahead๋ฅผ ํ•ฉ์ง‘ํ•ฉ์œผ๋กœ ๋งŒ๋“ญ๋‹ˆ๋‹ค. + * + * @param state1 ์ฒซ ๋ฒˆ์งธ ์ƒํƒœ + * @param state2 ๋‘ ๋ฒˆ์งธ ์ƒํƒœ + * @return ๋ณ‘ํ•ฉ๋œ ์ƒํƒœ + * @throws IllegalArgumentException ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์—†๋Š” ์ƒํƒœ๋“ค์ธ ๊ฒฝ์šฐ + */ + fun mergeLALR(state1: CompressedLRState, state2: CompressedLRState): CompressedLRState { + if (!canMergeLALR(state1, state2)) { + throw ParserException.lalrMergeNotAllowed( + state1 = state1, + state2 = state2, + reason = "๋‹ค๋ฅธ core ๋˜๋Š” lookahead ์ถฉ๋Œ" + ) + } + + val mergedItems = mutableSetOf() + + // ๋ชจ๋“  ์•„์ดํ…œ๋“ค์„ core ๊ธฐ์ค€์œผ๋กœ ๊ทธ๋ฃนํ™” + val allItems = (state1.coreItems + state2.coreItems) + .groupBy { "${it.production.id}:${it.dotPos}" } + + for ((_, items) in allItems) { + // ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ง„ ์•„์ดํ…œ๋“ค์˜ lookahead๋ฅผ ๋ชจ๋‘ ์ˆ˜์ง‘ + val production = items.first().production + val dotPos = items.first().dotPos + val allLookaheads = items.map { it.lookahead }.toSet() + + // ๊ฐ lookahead์— ๋Œ€ํ•ด ๋ณ„๋„์˜ ์•„์ดํ…œ ์ƒ์„ฑ + for (lookahead in allLookaheads) { + mergedItems.add(LRItem(production, dotPos, lookahead)) + } + } + + return CompressedLRState( + coreItems = mergedItems, + isBuilt = state1.isBuilt && state2.isBuilt + ) + } + + /** + * ๋นˆ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (ํ…Œ์ŠคํŠธ์šฉ). + * + * @return ๋นˆ ์ƒํƒœ + */ + fun empty(): CompressedLRState { + // ๋”๋ฏธ production๊ณผ item์œผ๋กœ ๋นˆ ์ƒํƒœ ์ƒ์„ฑ + val dummyProduction = Production( + id = -1, + left = TokenType.START, + right = emptyList() + ) + val dummyItem = LRItem(dummyProduction, 0, TokenType.DOLLAR) + + return CompressedLRState( + coreItems = setOf(dummyItem), + isBuilt = false + ) + } + } + + override fun toString(): String { + return "CompressedLRState(signature=$signature, items=${coreItems.size}, built=$isBuilt)" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/LRItem.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/LRItem.kt new file mode 100644 index 00000000..73d428d1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/LRItem.kt @@ -0,0 +1,321 @@ +package hs.kr.entrydsm.domain.parser.entities + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * LR(1) ํŒŒ์„œ์˜ ์•„์ดํ…œ์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. + * + * LR(1) ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ๊ตฌ์„ฑํ•˜๋Š” ๊ธฐ๋ณธ ๋‹จ์œ„๋กœ, ์ƒ์„ฑ ๊ทœ์น™๊ณผ ์ (โ€ข)์˜ ์œ„์น˜, + * ๊ทธ๋ฆฌ๊ณ  ์„ ํ–‰ ์‹ฌ๋ณผ(lookahead)๋กœ ๊ตฌ์„ฑ๋ฉ๋‹ˆ๋‹ค. ์•„์ดํ…œ์€ ํŒŒ์„œ๊ฐ€ ํ˜„์žฌ + * ์–ด๋–ค ์ƒ์„ฑ ๊ทœ์น™์„ ์–ผ๋งˆ๋‚˜ ์ธ์‹ํ–ˆ๋Š”์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋ฉฐ, LALR(1) ์ƒํƒœ ๊ตฌ์ถ•์˜ + * ํ•ต์‹ฌ ์š”์†Œ์ž…๋‹ˆ๋‹ค. + * + * @property production ์•„์ดํ…œ์ด ๊ธฐ๋ฐ˜ํ•˜๋Š” ์ƒ์„ฑ ๊ทœ์น™ + * @property dotPos ์ƒ์„ฑ ๊ทœ์น™ ์šฐ๋ณ€์—์„œ ์ (โ€ข)์˜ ์œ„์น˜ (0-based) + * @property lookahead ์„ ํ–‰ ์‹ฌ๋ณผ (๋‹ค์Œ์— ์˜ฌ ์ˆ˜ ์žˆ๋Š” ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(context = "parser", aggregateRoot = LRItem::class) +data class LRItem( + val production: Production, + val dotPos: Int, + val lookahead: TokenType +) { + + init { + if (dotPos < 0) { + throw ParserException.invalidDotPositionNegative(dotPos) + } + + if (dotPos > production.length) { + throw ParserException.invalidDotPositionExceeds(dotPos, production.length) + } + + if (!lookahead.isTerminal) { + throw ParserException.lookaheadNotTerminal(lookahead) + } + } + + /** + * ์ (โ€ข)์„ ํ•œ ์นธ ์•ž์œผ๋กœ ์ด๋™์‹œํ‚จ ์ƒˆ๋กœ์šด LRItem์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * ํ˜„์žฌ ์  ์œ„์น˜์—์„œ ๋‹ค์Œ ์‹ฌ๋ณผ์„ ์ฒ˜๋ฆฌํ•œ ํ›„์˜ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ƒˆ๋กœ์šด ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์ ์ด ์ด๋ฏธ ๋์— ์žˆ๋Š” ๊ฒฝ์šฐ IllegalStateException์„ ๋ฐœ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @return ์ ์ด ์ด๋™๋œ ์ƒˆ๋กœ์šด LRItem + * @throws IllegalStateException ์ ์ด ์ด๋ฏธ ๋์— ์žˆ๋Š” ๊ฒฝ์šฐ + */ + fun advance(): LRItem { + if (isComplete()) { + // this ๊ฐ€ LRItem์ธ ์ปจํ…์ŠคํŠธ + throw ParserException.itemAlreadyComplete(this) + } + + return copy(dotPos = dotPos + 1) + } + + /** + * ์•„์ดํ…œ์ด ์™„๋ฃŒ๋˜์—ˆ๋Š”์ง€ (์ (โ€ข)์ด ์ƒ์„ฑ ๊ทœ์น™ ์šฐ๋ณ€์˜ ๋์— ์žˆ๋Š”์ง€) ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * ์™„๋ฃŒ๋œ ์•„์ดํ…œ์€ reduce ์•ก์…˜์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์ƒํƒœ๋ฅผ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์•„์ดํ…œ์ด ์™„๋ฃŒ๋˜์—ˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isComplete(): Boolean = dotPos >= production.right.size + + /** + * ์ (โ€ข) ๋ฐ”๋กœ ๋‹ค์Œ์— ์˜ค๋Š” ์‹ฌ๋ณผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * ๋‹ค์Œ ์‹ฌ๋ณผ์ด ์žˆ๋Š” ๊ฒฝ์šฐ ํ•ด๋‹น ์‹ฌ๋ณผ์„, ์ ์ด ๋์— ์žˆ๋Š” ๊ฒฝ์šฐ null์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * ์ด ์‹ฌ๋ณผ์€ shift ๋˜๋Š” goto ์ „์ด์—์„œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @return ์  ๋‹ค์Œ์— ์˜ค๋Š” ์‹ฌ๋ณผ ๋˜๋Š” null (์ ์ด ๋์— ์žˆ๋Š” ๊ฒฝ์šฐ) + */ + fun nextSymbol(): TokenType? = if (dotPos < production.right.size) production.right[dotPos] else null + + /** + * ์ (โ€ข) ๋‹ค์Œ ์‹ฌ๋ณผ๋ถ€ํ„ฐ ์ƒ์„ฑ ๊ทœ์น™ ์šฐ๋ณ€์˜ ๋๊นŒ์ง€์˜ ์‹ฌ๋ณผ ์‹œํ€€์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ์ด๋‚˜ ํด๋กœ์ € ๊ตฌ์ถ• ์‹œ ์‚ฌ์šฉ๋˜๋Š” ๋ฒ ํƒ€(ฮฒ) ๋ถ€๋ถ„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + * ์  ๋‹ค์Œ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ธ ๊ฒฝ์šฐ, ํ•ด๋‹น ๋…ผํ„ฐ๋ฏธ๋„์˜ FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ์— ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์  ๋‹ค์Œ๋ถ€ํ„ฐ ๋๊นŒ์ง€์˜ ์‹ฌ๋ณผ ์‹œํ€€์Šค + */ + fun beta(): List = production.right.drop(dotPos + 1) + + /** + * ๋ฒ ํƒ€ ์‹œํ€€์Šค๊ฐ€ ๋น„์–ด์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฒ ํƒ€๊ฐ€ ๋น„์–ด์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isBetaEmpty(): Boolean = beta().isEmpty() + + /** + * ์  ์ด์ „์˜ ์‹ฌ๋ณผ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค (์•ŒํŒŒ ๋ถ€๋ถ„). + * + * ์ด๋ฏธ ์ฒ˜๋ฆฌ๋œ ์‹ฌ๋ณผ๋“ค์„ ๋‚˜ํƒ€๋‚ด๋ฉฐ, ๋””๋ฒ„๊น…์ด๋‚˜ ๋ถ„์„์— ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์  ์ด์ „๊นŒ์ง€์˜ ์‹ฌ๋ณผ ์‹œํ€€์Šค + */ + fun alpha(): List = production.right.take(dotPos) + + /** + * ์•„์ดํ…œ์˜ ํ•ต์‹ฌ(core) ๋ถ€๋ถ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * ํ•ต์‹ฌ์€ ์„ ํ–‰ ์‹ฌ๋ณผ์„ ์ œ์™ธํ•œ ์ƒ์„ฑ ๊ทœ์น™๊ณผ ์  ์œ„์น˜๋งŒ์„ ํฌํ•จํ•˜๋ฉฐ, + * LALR(1) ์ƒํƒœ ๋ณ‘ํ•ฉ ์‹œ ๋™์ผํ•œ ํ•ต์‹ฌ์„ ๊ฐ€์ง„ ์•„์ดํ…œ๋“ค์„ ์‹๋ณ„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @return ํ•ต์‹ฌ ์•„์ดํ…œ (์„ ํ–‰ ์‹ฌ๋ณผ์ด DOLLAR๋กœ ์„ค์ •๋จ) + */ + fun getCore(): LRItem = LRItem(production, dotPos, TokenType.DOLLAR) + + /** + * ๋‹ค๋ฅธ ์•„์ดํ…œ๊ณผ ํ•ต์‹ฌ์ด ๊ฐ™์€์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ์•„์ดํ…œ + * @return ํ•ต์‹ฌ์ด ๊ฐ™์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasSameCore(other: LRItem): Boolean = + production.id == other.production.id && dotPos == other.dotPos + + /** + * ์•„์ดํ…œ์ด kernel ์•„์ดํ…œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * Kernel ์•„์ดํ…œ์€ ์ ์ด ์ฒซ ๋ฒˆ์งธ ์œ„์น˜์— ์žˆ์ง€ ์•Š๊ฑฐ๋‚˜ ์‹œ์ž‘ ์•„์ดํ…œ์ธ ๊ฒฝ์šฐ์ž…๋‹ˆ๋‹ค. + * ํด๋กœ์ € ๊ตฌ์ถ• ์‹œ ์ดˆ๊ธฐ ์•„์ดํ…œ ์ง‘ํ•ฉ์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @return kernel ์•„์ดํ…œ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isKernelItem(): Boolean = dotPos > 0 || production.id == -1 // -1์€ ํ™•์žฅ๋œ ์‹œ์ž‘ ์ƒ์„ฑ ๊ทœ์น™ + + /** + * ์•„์ดํ…œ์ด ํด๋กœ์ €์— ์˜ํ•ด ์ถ”๊ฐ€๋œ ์•„์ดํ…œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํด๋กœ์ € ์•„์ดํ…œ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isClosureItem(): Boolean = !isKernelItem() + + /** + * ์  ๋‹ค์Œ ์‹ฌ๋ณผ์ด ํ„ฐ๋ฏธ๋„์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹ค์Œ ์‹ฌ๋ณผ์ด ํ„ฐ๋ฏธ๋„์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false (๋˜๋Š” ์‹ฌ๋ณผ์ด ์—†๋Š” ๊ฒฝ์šฐ) + */ + fun hasTerminalNext(): Boolean = nextSymbol()?.isTerminal == true + + /** + * ์  ๋‹ค์Œ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹ค์Œ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false (๋˜๋Š” ์‹ฌ๋ณผ์ด ์—†๋Š” ๊ฒฝ์šฐ) + */ + fun hasNonTerminalNext(): Boolean = nextSymbol()?.isNonTerminal() == true + + /** + * ์•„์ดํ…œ์ด ํŠน์ • ์‹ฌ๋ณผ๋กœ ์‹œ์ž‘ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ํ™•์ธํ•  ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ์‹ฌ๋ณผ๋กœ ์‹œ์ž‘ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun canShiftOn(symbol: TokenType): Boolean = nextSymbol() == symbol + + /** + * ์•„์ดํ…œ์ด reduce ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param inputSymbol ํ˜„์žฌ ์ž…๋ ฅ ์‹ฌ๋ณผ + * @return reduce ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun canReduceOn(inputSymbol: TokenType): Boolean = isComplete() && lookahead == inputSymbol + + /** + * ์„ ํ–‰ ์‹ฌ๋ณผ์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param newLookahead ์ƒˆ๋กœ์šด ์„ ํ–‰ ์‹ฌ๋ณผ + * @return ์ƒˆ๋กœ์šด ์„ ํ–‰ ์‹ฌ๋ณผ์„ ๊ฐ€์ง„ LRItem + */ + fun withLookahead(newLookahead: TokenType): LRItem = copy(lookahead = newLookahead) + + /** + * ์•„์ดํ…œ์˜ ๋ฌธ์ž์—ด ํ‘œํ˜„์—์„œ ์ ์˜ ์œ„์น˜๋ฅผ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ ์ด ํ‘œ์‹œ๋œ ์ƒ์„ฑ ๊ทœ์น™ ์šฐ๋ณ€ + */ + fun rightWithDot(): String { + val symbols = production.right.toMutableList() + symbols.add(dotPos, TokenType.DOLLAR) // ์ž„์‹œ๋กœ ์  ํ‘œ์‹œ์šฉ + return symbols.mapIndexed { i, sym -> + if (i == dotPos) "โ€ข" else sym.toString() + }.filter { it != "DOLLAR" }.joinToString(" ") + } + + /** + * ์•„์ดํ…œ์˜ ์ƒํƒœ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์•„์ดํ…œ ์ƒํƒœ ์ •๋ณด ๋งต + */ + fun getItemInfo(): Map = mapOf( + "productionId" to production.id, + "dotPos" to dotPos, + "lookahead" to lookahead, + "isComplete" to isComplete(), + "isKernel" to isKernelItem(), + "nextSymbol" to (nextSymbol()?.toString() ?: "none"), + "betaLength" to beta().size, + "alphaLength" to alpha().size + ) + + /** + * LRItem์„ ์‚ฌ๋žŒ์ด ์ฝ๊ธฐ ์‰ฌ์šด ๋ฌธ์ž์—ด ํ˜•ํƒœ๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * ์˜ˆ: [EXPR โ†’ EXPR โ€ข + TERM, $] + */ + override fun toString(): String { + val rightStr = if (production.right.isEmpty()) { + "โ€ข" + } else { + rightWithDot() + } + return "[${production.left} โ†’ $rightStr, $lookahead]" + } + + /** + * ๊ฐ„๋‹จํ•œ ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด ํ‘œํ˜„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "์ƒ์„ฑ๊ทœ์น™ID:์ ์œ„์น˜" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toShortString(): String = "${production.id}:$dotPos" + + /** + * ํ•ต์‹ฌ ์„œ๋ช…์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•ต์‹ฌ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ณ ์œ  ๋ฌธ์ž์—ด + */ + fun getCoreSignature(): String = "${production.id}:$dotPos" + + companion object { + /** + * ์‹œ์ž‘ ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param augmentedProduction ํ™•์žฅ๋œ ์‹œ์ž‘ ์ƒ์„ฑ ๊ทœ์น™ + * @return ์‹œ์ž‘ LRItem + */ + fun createStartItem(augmentedProduction: Production): LRItem = + LRItem(augmentedProduction, 0, TokenType.DOLLAR) + + /** + * ์•„์ดํ…œ ์ง‘ํ•ฉ์—์„œ kernel ์•„์ดํ…œ๋“ค๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param items ์•„์ดํ…œ ์ง‘ํ•ฉ + * @return kernel ์•„์ดํ…œ๋“ค์˜ ์ง‘ํ•ฉ + */ + fun extractKernelItems(items: Set): Set = + items.filter { it.isKernelItem() }.toSet() + + /** + * ์•„์ดํ…œ ์ง‘ํ•ฉ์—์„œ ํด๋กœ์ € ์•„์ดํ…œ๋“ค๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param items ์•„์ดํ…œ ์ง‘ํ•ฉ + * @return ํด๋กœ์ € ์•„์ดํ…œ๋“ค์˜ ์ง‘ํ•ฉ + */ + fun extractClosureItems(items: Set): Set = + items.filter { it.isClosureItem() }.toSet() + + /** + * ์•„์ดํ…œ๋“ค์„ ํ•ต์‹ฌ๋ณ„๋กœ ๊ทธ๋ฃนํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param items ๊ทธ๋ฃนํ™”ํ•  ์•„์ดํ…œ๋“ค + * @return ํ•ต์‹ฌ๋ณ„๋กœ ๊ทธ๋ฃนํ™”๋œ ๋งต + */ + fun groupByCore(items: Set): Map> = + items.groupBy { it.getCoreSignature() } + + /** + * ์•„์ดํ…œ ์ง‘ํ•ฉ๋“ค์ด ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param items1 ์ฒซ ๋ฒˆ์งธ ์•„์ดํ…œ ์ง‘ํ•ฉ + * @param items2 ๋‘ ๋ฒˆ์งธ ์•„์ดํ…œ ์ง‘ํ•ฉ + * @return ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun canMerge(items1: Set, items2: Set): Boolean { + val cores1 = items1.map { it.getCore() }.toSet() + val cores2 = items2.map { it.getCore() }.toSet() + return cores1 == cores2 + } + + /** + * ๋‘ ์•„์ดํ…œ ์ง‘ํ•ฉ์„ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param items1 ์ฒซ ๋ฒˆ์งธ ์•„์ดํ…œ ์ง‘ํ•ฉ + * @param items2 ๋‘ ๋ฒˆ์งธ ์•„์ดํ…œ ์ง‘ํ•ฉ + * @return ๋ณ‘ํ•ฉ๋œ ์•„์ดํ…œ ์ง‘ํ•ฉ + * @throws IllegalArgumentException ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun merge(items1: Set, items2: Set): Set { + if (!canMerge(items1, items2)) { + throw ParserException.itemSetMergeConflict("lookahead/core ์ถฉ๋Œ ๋˜๋Š” ์ •์ฑ… ์œ„๋ฐ˜") + } + + val mergedItems = mutableSetOf() + val allItems = items1 + items2 + + // ํ•ต์‹ฌ๋ณ„๋กœ ๊ทธ๋ฃนํ™”ํ•˜์—ฌ ์„ ํ–‰ ์‹ฌ๋ณผ๋“ค์„ ํ†ตํ•ฉ + val coreGroups = groupByCore(allItems) + + for ((_, itemsInCore) in coreGroups) { + val representative = itemsInCore.first() + val allLookaheads = itemsInCore.map { it.lookahead }.toSet() + + // ๊ฐ ์„ ํ–‰ ์‹ฌ๋ณผ์— ๋Œ€ํ•ด ๋ณ„๋„์˜ ์•„์ดํ…œ ์ƒ์„ฑ + for (lookahead in allLookaheads) { + mergedItems.add(LRItem(representative.production, representative.dotPos, lookahead)) + } + } + + return mergedItems + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/ParsingState.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/ParsingState.kt new file mode 100644 index 00000000..d3d772f0 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/ParsingState.kt @@ -0,0 +1,393 @@ +package hs.kr.entrydsm.domain.parser.entities + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.values.LRAction + +/** + * LR ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. + * + * LR ํŒŒ์„œ์—์„œ ๊ฐ ํŒŒ์‹ฑ ์ƒํƒœ๋Š” ๊ณ ์œ ํ•œ ID๋ฅผ ๊ฐ€์ง€๋ฉฐ, + * ํ•ด๋‹น ์ƒํƒœ์—์„œ ๊ฐ€๋Šฅํ•œ ์•„์ดํ…œ๋“ค์˜ ์ง‘ํ•ฉ๊ณผ ์ „์ด ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * DDD Entity ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์ƒํƒœ์˜ ๋™์ผ์„ฑ๊ณผ ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @property id ์ƒํƒœ์˜ ๊ณ ์œ  ์‹๋ณ„์ž + * @property items ์ด ์ƒํƒœ์—์„œ์˜ LR ์•„์ดํ…œ๋“ค + * @property transitions ๋‹ค๋ฅธ ์ƒํƒœ๋กœ์˜ ์ „์ด ๋งต + * @property actions ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ ์•ก์…˜ ๋งต + * @property gotos ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ goto ๋งต + * @property isAccepting ์ˆ˜๋ฝ ์ƒํƒœ ์—ฌ๋ถ€ + * @property isFinal ์ตœ์ข… ์ƒํƒœ ์—ฌ๋ถ€ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +data class ParsingState( + val id: Int, + val items: Set, + val transitions: Map = emptyMap(), + val actions: Map = emptyMap(), + val gotos: Map = emptyMap(), + val isAccepting: Boolean = false, + val isFinal: Boolean = false, + val metadata: Map = emptyMap() +) { + + init { + if (id < 0) { + throw ParserException.invalidStateId(id) + } + + if (items.isEmpty()) { + throw ParserException.emptyStateItems() + } + + if (isAccepting && !isFinal) { + throw ParserException.acceptingMustBeFinal(isAccepting, isFinal) + } + } + + companion object { + /** + * ์ดˆ๊ธฐ ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startItem ์‹œ์ž‘ LR ์•„์ดํ…œ + * @return ์ดˆ๊ธฐ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createInitial(startItem: LRItem): ParsingState { + return ParsingState( + id = 0, + items = setOf(startItem), + isAccepting = false, + isFinal = false + ) + } + + /** + * ์ˆ˜๋ฝ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒํƒœ ID + * @param items LR ์•„์ดํ…œ๋“ค + * @return ์ˆ˜๋ฝ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createAccepting(id: Int, items: Set): ParsingState { + return ParsingState( + id = id, + items = items, + isAccepting = true, + isFinal = true + ) + } + + /** + * ๋นˆ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (์—๋Ÿฌ ๋ณต๊ตฌ์šฉ). + * + * @param id ์ƒํƒœ ID + * @return ๋นˆ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createEmpty(id: Int): ParsingState { + val emptyItem = LRItem( + production = Production(-1, TokenType.START, listOf(TokenType.EPSILON)), + dotPos = 0, + lookahead = TokenType.DOLLAR + ) + + return ParsingState( + id = id, + items = setOf(emptyItem), + isFinal = true + ) + } + } + + /** + * ์ปค๋„ ์•„์ดํ…œ๋“ค (์บ์‹œ๋จ) + * ์ปค๋„ ์•„์ดํ…œ์€ ์ƒํƒœ๋ฅผ ๊ณ ์œ ํ•˜๊ฒŒ ์‹๋ณ„ํ•˜๋Š” ์•„์ดํ…œ๋“ค์ž…๋‹ˆ๋‹ค. + */ + val kernelItems: Set by lazy { + items.filter { it.isKernelItem() }.toSet() + } + + /** + * ๋น„์ปค๋„ ์•„์ดํ…œ๋“ค (์บ์‹œ๋จ) + * ๋น„์ปค๋„ ์•„์ดํ…œ์€ ํด๋กœ์ € ์—ฐ์‚ฐ์œผ๋กœ ์ถ”๊ฐ€๋œ ์•„์ดํ…œ๋“ค์ž…๋‹ˆ๋‹ค. + */ + val nonKernelItems: Set by lazy { + items.filter { !it.isKernelItem() }.toSet() + } + + /** + * ์ปค๋„ ์•„์ดํ…œ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค (๋ฉ”์†Œ๋“œ ํ˜•ํƒœ). + * + * @return ์ปค๋„ ์•„์ดํ…œ๋“ค์˜ ์ง‘ํ•ฉ + */ + @JvmName("getKernelItemsMethod") + fun getKernelItems(): Set = kernelItems + + /** + * ๋น„์ปค๋„ ์•„์ดํ…œ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค (๋ฉ”์†Œ๋“œ ํ˜•ํƒœ). + * + * @return ๋น„์ปค๋„ ์•„์ดํ…œ๋“ค์˜ ์ง‘ํ•ฉ + */ + @JvmName("getNonKernelItemsMethod") + fun getNonKernelItems(): Set = nonKernelItems + + /** + * ํŠน์ • ์‹ฌ๋ณผ๋กœ ์ „์ดํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ์ „์ดํ•  ์‹ฌ๋ณผ + * @return ์ „์ด ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun canTransition(symbol: TokenType): Boolean { + return symbol in transitions + } + + /** + * ํŠน์ • ์‹ฌ๋ณผ๋กœ ์ „์ดํ–ˆ์„ ๋•Œ์˜ ๋‹ค์Œ ์ƒํƒœ ID๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ์ „์ดํ•  ์‹ฌ๋ณผ + * @return ๋‹ค์Œ ์ƒํƒœ ID + * @throws IllegalArgumentException ์ „์ดํ•  ์ˆ˜ ์—†๋Š” ์‹ฌ๋ณผ์ธ ๊ฒฝ์šฐ + */ + fun getNextState(symbol: TokenType): Int { + return transitions[symbol] + ?: throw ParserException.transitionUnavailable(symbol) + } + + /** + * ํŠน์ • ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ ์•ก์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param terminal ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ์•ก์…˜ + */ + fun getAction(terminal: TokenType): LRAction? { + if (!terminal.isTerminal) { + throw ParserException.notATerminal(terminal) + } + + return actions[terminal] + } + + /** + * ํŠน์ • ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ goto๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param nonTerminal ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @return goto ์ƒํƒœ ID + */ + fun getGoto(nonTerminal: TokenType): Int? { + if (!nonTerminal.isNonTerminal()) { + throw ParserException.notANonTerminal(nonTerminal) + } + + return gotos[nonTerminal] + } + + /** + * ์ถฉ๋Œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ถฉ๋Œ ์ •๋ณด ๋งต + */ + fun getConflicts(): Map> { + val conflicts = mutableMapOf>() + + // ์™„๋ฃŒ๋œ ์•„์ดํ…œ๋“ค์„ ๋ฏธ๋ฆฌ ๊ณ„์‚ฐํ•˜์—ฌ ์žฌํ™œ์šฉ + val reduceItems = items.filter { it.isComplete() } + + // Shift/Reduce ์ถฉ๋Œ ๊ฒ€์‚ฌ + for ((terminal, action) in actions) { + if (action is LRAction.Shift) { + val reduceActions = reduceItems.filter { it.lookahead == terminal } + if (reduceActions.isNotEmpty()) { + conflicts.getOrPut("shift_reduce") { mutableListOf() } + .add("$terminal: shift vs reduce with ${reduceActions.map { it.production.id }}") + } + } + } + + // Reduce/Reduce ์ถฉ๋Œ ๊ฒ€์‚ฌ + for (terminal in actions.keys) { + val conflictingReduces = reduceItems.filter { it.lookahead == terminal } + if (conflictingReduces.size > 1) { + conflicts.getOrPut("reduce_reduce") { mutableListOf() } + .add("$terminal: multiple reduces ${conflictingReduces.map { it.production.id }}") + } + } + + return conflicts + } + + /** + * ์ƒํƒœ๊ฐ€ ์ผ๊ด€์„ฑ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ผ๊ด€์„ฑ์ด ์žˆ์œผ๋ฉด true + */ + fun isConsistent(): Boolean { + // 1. ๋ชจ๋“  ์•„์ดํ…œ์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ + if (items.isEmpty()) return false + + // 2. ์ถฉ๋Œ์ด ์—†๋Š”์ง€ ํ™•์ธ + if (getConflicts().isNotEmpty()) return false + + // 3. ์ „์ด ์ •๋ณด์˜ ์ผ๊ด€์„ฑ ํ™•์ธ + val itemSymbols = items.mapNotNull { it.nextSymbol() }.toSet() + val transitionSymbols = transitions.keys + + // ์•„์ดํ…œ์—์„œ ๋‚˜์˜ฌ ์ˆ˜ ์žˆ๋Š” ์‹ฌ๋ณผ๋“ค์ด ๋ชจ๋‘ ์ „์ด์— ํฌํ•จ๋˜์–ด์•ผ ํ•จ + return itemSymbols.all { symbol -> symbol in transitionSymbols } + } + + /** + * ์ƒํƒœ์˜ ์™„์„ฑ๋„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์™„์„ฑ๋„ (0.0 ~ 1.0) + */ + fun getCompleteness(): Double { + val totalItems = items.size + val completeItems = items.count { it.isComplete() } + return if (totalItems > 0) completeItems.toDouble() / totalItems else 0.0 + } + + /** + * ์ƒˆ๋กœ์šด ์ „์ด๋ฅผ ์ถ”๊ฐ€ํ•œ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ์ „์ด ์‹ฌ๋ณผ + * @param targetState ๋ชฉํ‘œ ์ƒํƒœ ID + * @return ์ „์ด๊ฐ€ ์ถ”๊ฐ€๋œ ์ƒˆ ์ƒํƒœ + */ + fun withTransition(symbol: TokenType, targetState: Int): ParsingState { + return copy(transitions = transitions + (symbol to targetState)) + } + + /** + * ์ƒˆ๋กœ์šด ์•ก์…˜์„ ์ถ”๊ฐ€ํ•œ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param terminal ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @param action ์•ก์…˜ + * @return ์•ก์…˜์ด ์ถ”๊ฐ€๋œ ์ƒˆ ์ƒํƒœ + */ + fun withAction(terminal: TokenType, action: LRAction): ParsingState { + if (!terminal.isTerminal) { + throw ParserException.notATerminal(terminal) + } + + return copy(actions = actions + (terminal to action)) + } + + /** + * ์ƒˆ๋กœ์šด goto๋ฅผ ์ถ”๊ฐ€ํ•œ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param nonTerminal ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @param targetState ๋ชฉํ‘œ ์ƒํƒœ ID + * @return goto๊ฐ€ ์ถ”๊ฐ€๋œ ์ƒˆ ์ƒํƒœ + */ + fun withGoto(nonTerminal: TokenType, targetState: Int): ParsingState { + if (!nonTerminal.isNonTerminal()) { + throw ParserException.notANonTerminal(nonTerminal) + } + + return copy(gotos = gotos + (nonTerminal to targetState)) + } + + /** + * ์ƒํƒœ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "id" to id, + "itemCount" to items.size, + "kernelItemCount" to kernelItems.size, + "nonKernelItemCount" to nonKernelItems.size, + "transitionCount" to transitions.size, + "actionCount" to actions.size, + "gotoCount" to gotos.size, + "completeness" to getCompleteness(), + "isAccepting" to isAccepting, + "isFinal" to isFinal, + "isConsistent" to isConsistent(), + "conflictCount" to getConflicts().values.sumOf { it.size } + ) + + /** + * ์ƒํƒœ๋ฅผ ์ƒ์„ธ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ์ •๋ณด ๋ฌธ์ž์—ด + */ + fun toDetailString(): String = buildString { + appendLine("State $id:") + appendLine(" Items:") + items.forEach { item -> + appendLine(" $item") + } + + if (transitions.isNotEmpty()) { + appendLine(" Transitions:") + transitions.forEach { (symbol, target) -> + appendLine(" $symbol -> $target") + } + } + + if (actions.isNotEmpty()) { + appendLine(" Actions:") + actions.forEach { (terminal, action) -> + appendLine(" $terminal: $action") + } + } + + if (gotos.isNotEmpty()) { + appendLine(" Gotos:") + gotos.forEach { (nonTerminal, target) -> + appendLine(" $nonTerminal -> $target") + } + } + + if (isAccepting) appendLine(" [ACCEPTING]") + if (isFinal) appendLine(" [FINAL]") + + val conflicts = getConflicts() + if (conflicts.isNotEmpty()) { + appendLine(" Conflicts:") + conflicts.forEach { (type, details) -> + appendLine(" $type: ${details.joinToString("; ")}") + } + } + } + + /** + * ์ƒํƒœ์˜ ๊ฐ„๋‹จํ•œ ์š”์•ฝ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์š”์•ฝ ๋ฌธ์ž์—ด + */ + override fun toString(): String = buildString { + append("State($id") + append(", items=${items.size}") + if (isAccepting) append(", ACCEPT") + if (isFinal) append(", FINAL") + val conflictCount = getConflicts().values.sumOf { it.size } + if (conflictCount > 0) append(", conflicts=$conflictCount") + append(")") + } + + /** + * ์ƒํƒœ์˜ ๋™์ผ์„ฑ์„ ID๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๊ฐ์ฒด + * @return ๋™์ผํ•˜๋ฉด true + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is ParsingState) return false + return id == other.id + } + + /** + * ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ํ•ด์‹œ ์ฝ”๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•ด์‹œ ์ฝ”๋“œ + */ + override fun hashCode(): Int = id.hashCode() +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/Production.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/Production.kt new file mode 100644 index 00000000..46b1472e --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/entities/Production.kt @@ -0,0 +1,324 @@ +package hs.kr.entrydsm.domain.parser.entities + +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.domain.ast.factory.ASTBuilders +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * ๋ฌธ๋ฒ•์˜ ์ƒ์„ฑ ๊ทœ์น™์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—”ํ‹ฐํ‹ฐ์ž…๋‹ˆ๋‹ค. + * + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์˜ BNF(Backus-Naur Form) ๋ฌธ๋ฒ• ๊ทœ์น™์„ ํ‘œํ˜„ํ•˜๋ฉฐ, ์ขŒ๋ณ€(๋…ผํ„ฐ๋ฏธ๋„), + * ์šฐ๋ณ€(์‹ฌ๋ณผ ์‹œํ€€์Šค), ๊ทธ๋ฆฌ๊ณ  ํ•ด๋‹น ๊ทœ์น™์„ ์ ์šฉํ•  ๋•Œ AST ๋…ธ๋“œ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋นŒ๋”๋ฅผ + * ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. LR(1) ํŒŒ์„œ์—์„œ reduce ๋™์ž‘ ์‹œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @property id ์ƒ์„ฑ ๊ทœ์น™์˜ ๊ณ ์œ  ์‹๋ณ„์ž + * @property left ์ƒ์„ฑ ๊ทœ์น™์˜ ์ขŒ๋ณ€ (๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ) + * @property right ์ƒ์„ฑ ๊ทœ์น™์˜ ์šฐ๋ณ€ (์‹ฌ๋ณผ ์‹œํ€€์Šค) + * @property astBuilder AST ๋…ธ๋“œ๋ฅผ ๊ตฌ์ถ•ํ•˜๋Š” ๋นŒ๋” + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(context = "parser", aggregateRoot = Production::class) +data class Production( + val id: Int, + val left: TokenType, + val right: List, + val astBuilder: ASTBuilderContract = ASTBuilders.Identity +) { + + init { + if (id < -1) { + throw ParserException.productionIdBelowMin(id) + } + + if (!left.isNonTerminal()) { + throw ParserException.productionLeftNotNonTerminal(left) + } + + if (right.isEmpty() && !isEpsilonProduction()) { + throw ParserException.productionRightEmpty() + } + } + + /** + * ์ƒ์„ฑ ๊ทœ์น™ ์šฐ๋ณ€์˜ ๊ธธ์ด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ๋ณ€ ์‹ฌ๋ณผ์˜ ๊ฐœ์ˆ˜ + */ + val length: Int = right.size + + /** + * ์—ก์‹ค๋ก  ์ƒ์„ฑ ๊ทœ์น™์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ก์‹ค๋ก  ์ƒ์„ฑ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isEpsilonProduction(): Boolean = right.isEmpty() + + /** + * ํŠน์ • ์œ„์น˜์˜ ์‹ฌ๋ณผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param position ์‹ฌ๋ณผ ์œ„์น˜ (0-based) + * @return ํ•ด๋‹น ์œ„์น˜์˜ ์‹ฌ๋ณผ + * @throws IndexOutOfBoundsException ์œ„์น˜๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚œ ๊ฒฝ์šฐ + */ + fun getSymbolAt(position: Int): TokenType { + if (position !in right.indices) { + throw ParserException.productionPositionOutOfRange(position, right.size - 1) + } + + return right[position] + } + + /** + * ํŠน์ • ์œ„์น˜๊นŒ์ง€์˜ ์‹ฌ๋ณผ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param endPosition ๋ ์œ„์น˜ (ํฌํ•จํ•˜์ง€ ์•Š์Œ) + * @return ์ง€์ •๋œ ๋ฒ”์œ„์˜ ์‹ฌ๋ณผ ๋ฆฌ์ŠคํŠธ + */ + fun getSymbolsUntil(endPosition: Int): List { + if (endPosition < 0) { + throw ParserException.endPositionNegative(endPosition) + } + + if (endPosition > right.size) { + throw ParserException.endPositionExceeds(endPosition, right.size) + } + + return right.take(endPosition) + } + + /** + * ํŠน์ • ์œ„์น˜๋ถ€ํ„ฐ์˜ ์‹ฌ๋ณผ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param startPosition ์‹œ์ž‘ ์œ„์น˜ + * @return ์ง€์ •๋œ ์œ„์น˜๋ถ€ํ„ฐ์˜ ์‹ฌ๋ณผ ๋ฆฌ์ŠคํŠธ + */ + fun getSymbolsFrom(startPosition: Int): List { + if (startPosition < 0) { + throw ParserException.startPositionNegative(startPosition) + } + + if (startPosition > right.size) { + throw ParserException.startPositionExceeds(startPosition, right.size) + } + + return right.drop(startPosition) + } + + /** + * ์šฐ๋ณ€์—์„œ ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ๊ฐœ์ˆ˜ + */ + fun getTerminalCount(): Int = right.count { it.isTerminal } + + /** + * ์šฐ๋ณ€์—์„œ ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ๊ฐœ์ˆ˜ + */ + fun getNonTerminalCount(): Int = right.count { it.isNonTerminal() } + + /** + * ์šฐ๋ณ€์— ํŠน์ • ์‹ฌ๋ณผ์ด ํฌํ•จ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ํ™•์ธํ•  ์‹ฌ๋ณผ + * @return ํฌํ•จ๋˜์–ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun containsSymbol(symbol: TokenType): Boolean = symbol in right + + /** + * ์šฐ๋ณ€์—์„œ ํŠน์ • ์‹ฌ๋ณผ์˜ ์ฒซ ๋ฒˆ์งธ ์œ„์น˜๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param symbol ์ฐพ์„ ์‹ฌ๋ณผ + * @return ์ฒซ ๋ฒˆ์งธ ์œ„์น˜ (์—†์œผ๋ฉด -1) + */ + fun findSymbolPosition(symbol: TokenType): Int = right.indexOf(symbol) + + /** + * ์šฐ๋ณ€์—์„œ ํŠน์ • ์‹ฌ๋ณผ์˜ ๋ชจ๋“  ์œ„์น˜๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param symbol ์ฐพ์„ ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ์‹ฌ๋ณผ์˜ ๋ชจ๋“  ์œ„์น˜ ๋ฆฌ์ŠคํŠธ + */ + fun findAllSymbolPositions(symbol: TokenType): List = + right.mapIndexedNotNull { index, sym -> if (sym == symbol) index else null } + + /** + * ์šฐ๋ณ€์ด ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋กœ๋งŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ชจ๋‘ ํ„ฐ๋ฏธ๋„์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAllTerminals(): Boolean = right.all { it.isTerminal } + + /** + * ์šฐ๋ณ€์ด ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋กœ๋งŒ ๊ตฌ์„ฑ๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ชจ๋‘ ๋…ผํ„ฐ๋ฏธ๋„์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAllNonTerminals(): Boolean = right.all { it.isNonTerminal() } + + /** + * ์ƒ์„ฑ ๊ทœ์น™์ด ์ง์ ‘ ์ขŒ์žฌ๊ท€์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง์ ‘ ์ขŒ์žฌ๊ท€์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isDirectLeftRecursive(): Boolean = right.isNotEmpty() && right[0] == left + + /** + * ์ƒ์„ฑ ๊ทœ์น™์ด ์ง์ ‘ ์šฐ์žฌ๊ท€์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง์ ‘ ์šฐ์žฌ๊ท€์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isDirectRightRecursive(): Boolean = right.isNotEmpty() && right.last() == left + + /** + * AST ๋นŒ๋”๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ AST ๋…ธ๋“œ๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + * + * @param children ์ž์‹ ์‹ฌ๋ณผ๋“ค + * @return ๊ตฌ์ถ•๋œ AST ๋…ธ๋“œ ๋˜๋Š” ์‹ฌ๋ณผ + * @throws IllegalArgumentException ์ž์‹ ์‹ฌ๋ณผ์˜ ๊ฐœ์ˆ˜๋‚˜ ํƒ€์ž…์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun buildAST(children: List): Any { + if (!astBuilder.validateChildren(children)) { + throw ParserException.astBuilderValidationFailed(id, children.size) + } + return astBuilder.build(children) + } + + + /** + * ์ƒ์„ฑ ๊ทœ์น™์„ BNF ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "์ขŒ๋ณ€ โ†’ ์šฐ๋ณ€" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toBNFString(): String = "$left โ†’ ${if (right.isEmpty()) "ฮต" else right.joinToString(" ")}" + + /** + * ์ƒ์„ฑ ๊ทœ์น™์„ ์ƒ์„ธ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "ID: ์ขŒ๋ณ€ โ†’ ์šฐ๋ณ€ [๋นŒ๋”์ •๋ณด]" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toDetailString(): String = "$id: ${toBNFString()} [${astBuilder.getBuilderName()}]" + + /** + * ์ƒ์„ฑ ๊ทœ์น™์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "id" to id, + "length" to length, + "terminalCount" to getTerminalCount(), + "nonTerminalCount" to getNonTerminalCount(), + "isEpsilon" to isEpsilonProduction(), + "isDirectLeftRecursive" to isDirectLeftRecursive(), + "isDirectRightRecursive" to isDirectRightRecursive(), + "builderType" to astBuilder.getBuilderName() + ) + + /** + * ์ƒ์„ฑ ๊ทœ์น™์„ ๊ฐ„๋‹จํ•œ ํ˜•ํƒœ๋กœ ๋ฌธ์ž์—ด ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return BNF ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + override fun toString(): String = toBNFString() + + companion object { + /** + * ์—ก์‹ค๋ก  ์ƒ์„ฑ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param astBuilder AST ๋นŒ๋” (๊ธฐ๋ณธ๊ฐ’: Identity) + * @return ์—ก์‹ค๋ก  ์ƒ์„ฑ ๊ทœ์น™ + */ + fun epsilon(id: Int, left: TokenType, astBuilder: ASTBuilderContract = ASTBuilders.Identity): Production = + Production(id, left, emptyList(), astBuilder) + + /** + * ๋‹จ์ผ ์‹ฌ๋ณผ ์ƒ์„ฑ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param right ์šฐ๋ณ€ ์‹ฌ๋ณผ + * @param astBuilder AST ๋นŒ๋” (๊ธฐ๋ณธ๊ฐ’: Identity) + * @return ๋‹จ์ผ ์‹ฌ๋ณผ ์ƒ์„ฑ ๊ทœ์น™ + */ + fun single(id: Int, left: TokenType, right: TokenType, astBuilder: ASTBuilderContract = ASTBuilders.Identity): Production = + Production(id, left, listOf(right), astBuilder) + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ์ž ์ƒ์„ฑ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param leftOperand ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @param operator ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @param rightOperand ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @param operatorString ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @return ์ดํ•ญ ์—ฐ์‚ฐ์ž ์ƒ์„ฑ ๊ทœ์น™ + */ + fun binaryOp( + id: Int, + left: TokenType, + leftOperand: TokenType, + operator: TokenType, + rightOperand: TokenType, + operatorString: String + ): Production = Production( + id, + left, + listOf(leftOperand, operator, rightOperand), + ASTBuilders.createBinaryOp(operatorString) + ) + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ์ƒ์„ฑ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param operator ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @param operand ํ”ผ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @param operatorString ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @return ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ์ƒ์„ฑ ๊ทœ์น™ + */ + fun unaryOp( + id: Int, + left: TokenType, + operator: TokenType, + operand: TokenType, + operatorString: String + ): Production = Production( + id, + left, + listOf(operator, operand), + ASTBuilders.createUnaryOp(operatorString) + ) + + /** + * ์ƒ์„ฑ ๊ทœ์น™ ๋ฆฌ์ŠคํŠธ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ๊ฒ€์ฆํ•  ์ƒ์„ฑ ๊ทœ์น™ ๋ฆฌ์ŠคํŠธ + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun validateProductions(productions: List): Boolean { + if (productions.isEmpty()) return false + + // ID ์ค‘๋ณต ๊ฒ€์‚ฌ + val ids = productions.map { it.id } + if (ids.size != ids.toSet().size) return false + + // ID ์—ฐ์†์„ฑ ๊ฒ€์‚ฌ (0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ์—ฐ์†๋œ ๋ฒˆํ˜ธ) + val sortedIds = ids.sorted() + if (sortedIds != (0 until productions.size).toList()) return false + + return true + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/exceptions/ParserException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/exceptions/ParserException.kt new file mode 100644 index 00000000..3019c462 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/exceptions/ParserException.kt @@ -0,0 +1,1755 @@ +package hs.kr.entrydsm.domain.parser.exceptions + +import hs.kr.entrydsm.domain.parser.entities.CompressedLRState +import hs.kr.entrydsm.global.exception.DomainException +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * Parser ๋„๋ฉ”์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * LR(1) ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ๊ตฌ๋ฌธ ์˜ค๋ฅ˜, GOTO ์ƒํƒœ ์ „์ด ์˜ค๋ฅ˜, + * ๋ฌธ๋ฒ• ์ถฉ๋Œ, ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์ž…๋ ฅ ์ข…๋ฃŒ ๋“ฑ์˜ ๊ตฌ๋ฌธ ๋ถ„์„ ๊ด€๋ จ ์˜ค๋ฅ˜๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @property state ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ํŒŒ์„œ ์ƒํƒœ (์„ ํƒ์‚ฌํ•ญ) + * @property expectedTokens ์˜ˆ์ƒ๋œ ํ† ํฐ ๋ฆฌ์ŠคํŠธ (์„ ํƒ์‚ฌํ•ญ) + * @property actualToken ์‹ค์ œ ๋ฐ›์€ ํ† ํฐ (์„ ํƒ์‚ฌํ•ญ) + * @property production ๊ด€๋ จ๋œ ์ƒ์„ฑ ๊ทœ์น™ (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +class ParserException( + errorCode: ErrorCode, + val state: Int? = null, + val expectedTokens: List = emptyList(), + val actualToken: String? = null, + val production: String? = null, + message: String = buildParserMessage(errorCode, state, expectedTokens, actualToken, production), + cause: Throwable? = null +) : DomainException(errorCode, message, cause) { + + companion object { + /** + * Parser ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param errorCode ์˜ค๋ฅ˜ ์ฝ”๋“œ + * @param state ํŒŒ์„œ ์ƒํƒœ + * @param expectedTokens ์˜ˆ์ƒ ํ† ํฐ๋“ค + * @param actualToken ์‹ค์ œ ํ† ํฐ + * @param production ์ƒ์„ฑ ๊ทœ์น™ + * @return ๊ตฌ์„ฑ๋œ ๋ฉ”์‹œ์ง€ + */ + private fun buildParserMessage( + errorCode: ErrorCode, + state: Int?, + expectedTokens: List, + actualToken: String?, + production: String? + ): String { + val baseMessage = errorCode.description + val details = mutableListOf() + + state?.let { details.add("์ƒํƒœ: $it") } + if (expectedTokens.isNotEmpty()) { + details.add("์˜ˆ์ƒ: ${expectedTokens.joinToString(", ")}") + } + actualToken?.let { details.add("์‹ค์ œ: $it") } + production?.let { details.add("๊ทœ์น™: $it") } + + return if (details.isNotEmpty()) { + "$baseMessage (${details.joinToString(", ")})" + } else { + baseMessage + } + } + + /** + * ๊ตฌ๋ฌธ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expectedTokens ์˜ˆ์ƒ๋œ ํ† ํฐ๋“ค + * @param actualToken ์‹ค์ œ ํ† ํฐ + * @param state ํŒŒ์„œ ์ƒํƒœ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun syntaxError(expectedTokens: List, actualToken: String, state: Int): ParserException { + return ParserException( + errorCode = ErrorCode.SYNTAX_ERROR, + expectedTokens = expectedTokens, + actualToken = actualToken, + state = state + ) + } + + /** + * ๊ตฌ๋ฌธ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (ํ† ํฐ ๊ฐ์ฒด ๋ฒ„์ „). + * + * @param currentToken ํ˜„์žฌ ํ† ํฐ + * @param currentState ํ˜„์žฌ ์ƒํƒœ + * @param errorMessage ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun syntaxError(currentToken: Any, currentState: Any, errorMessage: String): ParserException { + return ParserException( + errorCode = ErrorCode.SYNTAX_ERROR, + state = currentState.toString().toIntOrNull(), + actualToken = currentToken.toString(), + message = errorMessage + ) + } + + /** + * GOTO ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ํ˜„์žฌ ์ƒํƒœ + * @param symbol GOTO ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun gotoError(state: Int, symbol: String): ParserException { + return ParserException( + errorCode = ErrorCode.GOTO_ERROR, + state = state, + actualToken = symbol + ) + } + + /** + * LR ํŒŒ์‹ฑ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ํŒŒ์„œ ์ƒํƒœ + * @param token ๋ฌธ์ œ๊ฐ€ ๋œ ํ† ํฐ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun lrParsingError(state: Int, token: String): ParserException { + return ParserException( + errorCode = ErrorCode.LR_PARSING_ERROR, + state = state, + actualToken = token + ) + } + + /** + * ๋ฌธ๋ฒ• ์ถฉ๋Œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param production ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์ƒ์„ฑ ๊ทœ์น™ + * @param state ํŒŒ์„œ ์ƒํƒœ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun grammarConflict(production: String, state: Int): ParserException { + return ParserException( + errorCode = ErrorCode.GRAMMAR_CONFLICT, + production = production, + state = state + ) + } + + /** + * ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์ž…๋ ฅ ์ข…๋ฃŒ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expectedTokens ์˜ˆ์ƒ๋œ ํ† ํฐ๋“ค + * @param state ํŒŒ์„œ ์ƒํƒœ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unexpectedEndOfInput(expectedTokens: List, state: Int): ParserException { + return ParserException( + errorCode = ErrorCode.UNEXPECTED_END_OF_INPUT, + expectedTokens = expectedTokens, + state = state + ) + } + + /** + * ์ž˜๋ชป๋œ AST ๋…ธ๋“œ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ์ž˜๋ชป๋œ ๊ฒฐ๊ณผ ๊ฐ์ฒด + * @param state ํŒŒ์„œ ์ƒํƒœ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun invalidASTNode(result: Any?, state: Int? = null): ParserException { + return ParserException( + errorCode = ErrorCode.INVALID_AST_NODE, + state = state, + message = "์ž˜๋ชป๋œ AST ๋…ธ๋“œ: ${result?.javaClass?.simpleName ?: "null"}" + ) + } + + /** + * ์Šคํƒ ์˜ค๋ฒ„ํ”Œ๋กœ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxStackSize ์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun stackOverflow(maxStackSize: Int): ParserException { + return ParserException( + errorCode = ErrorCode.STACK_OVERFLOW, + message = "ํŒŒ์„œ ์Šคํƒ์ด ์ตœ๋Œ€ ํฌ๊ธฐ($maxStackSize)๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค" + ) + } + + /** + * ๋ถˆ์™„์ „ํ•œ ์ž…๋ ฅ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param message ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ (์„ ํƒ์‚ฌํ•ญ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun incompleteInput(message: String = "์ž…๋ ฅ์ด ๋ถˆ์™„์ „ํ•ฉ๋‹ˆ๋‹ค"): ParserException { + return ParserException( + errorCode = ErrorCode.INCOMPLETE_INPUT, + message = message + ) + } + + /** + * ์ผ๋ฐ˜ ํŒŒ์‹ฑ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param message ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ + * @param state ํŒŒ์„œ ์ƒํƒœ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun parsingError(message: String, state: Int? = null): ParserException { + return ParserException( + errorCode = ErrorCode.PARSING_ERROR, + state = state, + message = message + ) + } + + /** + * ์˜ˆ์™ธ๋กœ๋ถ€ํ„ฐ ํŒŒ์‹ฑ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param exception ์›์ธ ์˜ˆ์™ธ + * @param state ํŒŒ์„œ ์ƒํƒœ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun parsingError(exception: Exception, state: Int? = null): ParserException { + return ParserException( + errorCode = ErrorCode.PARSING_ERROR, + state = state, + message = "ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${exception.message}", + cause = exception + ) + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์ด ๋น„์–ด์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun emptyProductions(): ParserException = + ParserException( + errorCode = ErrorCode.EMPTY_PRODUCTIONS, + message = "์ƒ์‚ฐ ๊ทœ์น™์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ์ด ๋น„์–ด์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun emptyTerminals(): ParserException = + ParserException( + errorCode = ErrorCode.EMPTY_TERMINALS, + message = "ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ์ด ๋น„์–ด์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun emptyNonTerminals(): ParserException = + ParserException( + errorCode = ErrorCode.EMPTY_NON_TERMINALS, + message = "๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ์‹œ์ž‘ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ด ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startSymbol ์‹œ์ž‘ ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun invalidStartSymbol(startSymbol: Any?): ParserException = + ParserException( + errorCode = ErrorCode.INVALID_START_SYMBOL, + message = "์‹œ์ž‘ ์‹ฌ๋ณผ์€ ๋…ผํ„ฐ๋ฏธ๋„์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค (์ž…๋ ฅ: $startSymbol)" + ) + + /** + * ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๊ฐ€ 0 ์ดํ•˜์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxDepth ๊ตฌ์„ฑ๋œ ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun maxDepthNonPositive(maxDepth: Int): ParserException = + ParserException( + errorCode = ErrorCode.MAX_DEPTH_NON_POSITIVE, + message = "์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค: $maxDepth" + ) + + /** + * ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๊ฐ€ ํ—ˆ์šฉ ํ•œ๊ณ„๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxDepth ๊ตฌ์„ฑ๋œ ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด + * @param limit ํ—ˆ์šฉ ํ•œ๊ณ„(์Šคํƒ ํ•œ๊ณ„) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun maxDepthExceedsLimit(maxDepth: Int, limit: Int): ParserException = + ParserException( + errorCode = ErrorCode.MAX_DEPTH_EXCEEDS_LIMIT, + message = "์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๊ฐ€ ํ•œ๊ณ„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $maxDepth > $limit" + ) + + /** + * Core ์•„์ดํ…œ ์ง‘ํ•ฉ์ด ๋น„์–ด์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun emptyCoreItems(): ParserException = + ParserException( + errorCode = ErrorCode.CORE_ITEMS_EMPTY, + message = "Core ์•„์ดํ…œ์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ์•„์ดํ…œ ์ง‘ํ•ฉ์ด ๋น„์–ด์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun emptyItems(): ParserException = + ParserException( + errorCode = ErrorCode.ITEMS_EMPTY, + message = "์•„์ดํ…œ ์ง‘ํ•ฉ์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ๋‘ ์ƒํƒœ๋ฅผ LALR ์ •์ฑ…์œผ๋กœ ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param state1 ์ฒซ ๋ฒˆ์งธ ์ƒํƒœ + * @param state2 ๋‘ ๋ฒˆ์งธ ์ƒํƒœ + * @param reason ๋ณ‘ํ•ฉ ์‹คํŒจ ์‚ฌ์œ (์„ ํƒ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun lalrMergeNotAllowed( + state1: CompressedLRState, + state2: CompressedLRState, + reason: String? = null + ): ParserException = + ParserException( + errorCode = ErrorCode.LALR_MERGE_CONFLICT, + message = buildString { + append("์ƒํƒœ๋“ค์„ LALR ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค") + append(": ") + append(reason ?: "๋‹ค๋ฅธ core ๋˜๋Š” lookahead ์ถฉ๋Œ") + append(" (state1="); append(state1); append(", state2="); append(state2); append(")") + } + ) + + /** + * ์  ์œ„์น˜๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param dotPos ์ ์˜ ํ˜„์žฌ ์œ„์น˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun invalidDotPositionNegative(dotPos: Int): ParserException = + ParserException( + errorCode = ErrorCode.DOT_POSITION_NEGATIVE, + message = "์ ์˜ ์œ„์น˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $dotPos" + ) + + /** + * ์  ์œ„์น˜๊ฐ€ ์ƒ์„ฑ ๊ทœ์น™์˜ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param dotPos ์ ์˜ ํ˜„์žฌ ์œ„์น˜ + * @param productionLength ๋Œ€์ƒ ์ƒ์„ฑ ๊ทœ์น™์˜ ๊ธธ์ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun invalidDotPositionExceeds(dotPos: Int, productionLength: Int): ParserException = + ParserException( + errorCode = ErrorCode.DOT_POSITION_EXCEEDS_LENGTH, + message = "์ ์˜ ์œ„์น˜๊ฐ€ ์ƒ์„ฑ ๊ทœ์น™ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $dotPos > $productionLength" + ) + + /** + * lookahead ์‹ฌ๋ณผ์ด ํ„ฐ๋ฏธ๋„์ด ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param lookahead ์„ ํ–‰(lookahead) ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun lookaheadNotTerminal( + lookahead: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.LOOKAHEAD_NOT_TERMINAL, + message = "์„ ํ–‰ ์‹ฌ๋ณผ์€ ํ„ฐ๋ฏธ๋„์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $lookahead" + ) + + /** + * ์™„๋ฃŒ๋œ ์•„์ดํ…œ์— ๋Œ€ํ•ด ์ ์„ ์ด๋™ํ•˜๋ ค ํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param item ๋Œ€์ƒ LR ์•„์ดํ…œ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun itemAlreadyComplete( + item: hs.kr.entrydsm.domain.parser.entities.LRItem + ): ParserException = + ParserException( + errorCode = ErrorCode.ITEM_ALREADY_COMPLETE, + message = "์™„๋ฃŒ๋œ ์•„์ดํ…œ์˜ ์ ์„ ๋” ์ด์ƒ ์ด๋™์‹œํ‚ฌ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $item" + ) + + /** + * ๋‘ ์•„์ดํ…œ ์ง‘ํ•ฉ์„ ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param reason ๋ณ‘ํ•ฉ ๋ถˆ๊ฐ€ ์‚ฌ์œ (์„ ํƒ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun itemSetMergeConflict(reason: String? = null): ParserException = + ParserException( + errorCode = ErrorCode.ITEM_SET_MERGE_CONFLICT, + message = "์•„์ดํ…œ ์ง‘ํ•ฉ๋“ค์„ ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + + (reason?.let { ": $it" } ?: "") + ) + + /** + * ์ƒํƒœ ID๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒํƒœ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun invalidStateId(id: Int): ParserException = + ParserException( + errorCode = ErrorCode.STATE_ID_NEGATIVE, + message = "์ƒํƒœ ID๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $id" + ) + + /** + * ํŒŒ์‹ฑ ์ƒํƒœ๊ฐ€ ์ตœ์†Œ ํ•˜๋‚˜์˜ LR ์•„์ดํ…œ์„ ํฌํ•จํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun emptyStateItems(): ParserException = + ParserException( + errorCode = ErrorCode.STATE_ITEMS_EMPTY, + message = "ํŒŒ์‹ฑ ์ƒํƒœ๋Š” ์ตœ์†Œ ํ•˜๋‚˜์˜ LR ์•„์ดํ…œ์„ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + + /** + * ์ˆ˜๋ฝ(accepting) ์ƒํƒœ๊ฐ€ ์ตœ์ข…(final) ์ƒํƒœ๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param isAccepting ์ˆ˜๋ฝ ์ƒํƒœ ์—ฌ๋ถ€ + * @param isFinal ์ตœ์ข… ์ƒํƒœ ์—ฌ๋ถ€ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun acceptingMustBeFinal(isAccepting: Boolean, isFinal: Boolean): ParserException = + ParserException( + errorCode = ErrorCode.ACCEPTING_MUST_BE_FINAL, + message = "์ˆ˜๋ฝ ์ƒํƒœ๋Š” ๋ฐ˜๋“œ์‹œ ์ตœ์ข… ์ƒํƒœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค (accepting=$isAccepting, final=$isFinal)" + ) + + /** + * ์ฃผ์–ด์ง„ ์‹ฌ๋ณผ๋กœ ์ „์ดํ•  ์ˆ˜ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ์ „์ด ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun transitionUnavailable( + symbol: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.TRANSITION_UNAVAILABLE, + message = "์‹ฌ๋ณผ $symbol ๋กœ ์ „์ดํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ์ฃผ์–ด์ง„ ์‹ฌ๋ณผ์ด ํ„ฐ๋ฏธ๋„์ด ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param terminal ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun notATerminal( + terminal: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.NOT_TERMINAL_SYMBOL, + message = "ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์•„๋‹™๋‹ˆ๋‹ค: $terminal" + ) + + /** + * ์ฃผ์–ด์ง„ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ด ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nonTerminal ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun notANonTerminal( + nonTerminal: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.NOT_NON_TERMINAL_SYMBOL, + message = "๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์•„๋‹™๋‹ˆ๋‹ค: $nonTerminal" + ) + + /** + * ์ƒ์„ฑ ๊ทœ์น™ ID๊ฐ€ ํ—ˆ์šฉ ์ตœ์†Œ๊ฐ’(-1)๋ณด๋‹ค ์ž‘์€ ๊ฒฝ์šฐ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์„ฑ ๊ทœ์น™ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionIdBelowMin(id: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_ID_BELOW_MIN, + message = "์ƒ์„ฑ ๊ทœ์น™ ID๋Š” -1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $id" + ) + + /** + * ์ƒ์„ฑ ๊ทœ์น™์˜ ์ขŒ๋ณ€ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ด ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionLeftNotNonTerminal( + left: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_LEFT_NOT_NON_TERMINAL, + message = "์ƒ์„ฑ ๊ทœ์น™์˜ ์ขŒ๋ณ€์€ ๋…ผํ„ฐ๋ฏธ๋„์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $left" + ) + + /** + * ์ƒ์„ฑ ๊ทœ์น™์˜ ์šฐ๋ณ€์ด ๋น„์–ด์žˆ๋Š”๋ฐ ์—ก์‹ค๋ก  ์ƒ์„ฑ์ด ์•„๋‹Œ ๊ฒฝ์šฐ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionRightEmpty(): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_RIGHT_EMPTY, + message = "์ƒ์„ฑ ๊ทœ์น™์˜ ์šฐ๋ณ€์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (์—ก์‹ค๋ก  ์ƒ์„ฑ ์ œ์™ธ)" + ) + + /** + * ํฌ์ธํ„ฐ ์œ„์น˜๊ฐ€ ์šฐ๋ณ€ ์ธ๋ฑ์Šค ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param position ํ˜„์žฌ ์œ„์น˜ + * @param maxIndex ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์ธ๋ฑ์Šค(right.size - 1) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionPositionOutOfRange(position: Int, maxIndex: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_POSITION_OUT_OF_RANGE, + message = "์œ„์น˜๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค: $position, ๋ฒ”์œ„: 0-$maxIndex" + ) + + /** + * ๋ ์œ„์น˜(endPosition)๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param endPosition ๋ ์œ„์น˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun endPositionNegative(endPosition: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_END_POSITION_NEGATIVE, + message = "๋ ์œ„์น˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $endPosition" + ) + + /** + * ๋ ์œ„์น˜(endPosition)๊ฐ€ ์šฐ๋ณ€์˜ ํฌ๊ธฐ๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param endPosition ๋ ์œ„์น˜ + * @param size ์šฐ๋ณ€ ํฌ๊ธฐ(right.size) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun endPositionExceeds(endPosition: Int, size: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_END_POSITION_EXCEEDS, + message = "๋ ์œ„์น˜๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค: $endPosition > $size" + ) + + /** + * ์‹œ์ž‘ ์œ„์น˜(startPosition)๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startPosition ์‹œ์ž‘ ์œ„์น˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun startPositionNegative(startPosition: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_START_POSITION_NEGATIVE, + message = "์‹œ์ž‘ ์œ„์น˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $startPosition" + ) + + /** + * ์‹œ์ž‘ ์œ„์น˜(startPosition)๊ฐ€ ์šฐ๋ณ€์˜ ํฌ๊ธฐ๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startPosition ์‹œ์ž‘ ์œ„์น˜ + * @param size ์šฐ๋ณ€ ํฌ๊ธฐ(right.size) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun startPositionExceeds(startPosition: Int, size: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_START_POSITION_EXCEEDS, + message = "์‹œ์ž‘ ์œ„์น˜๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค: $startPosition > $size" + ) + + /** + * AST ๋นŒ๋”๊ฐ€ ์ž์‹ ๋…ธ๋“œ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param ruleId ์ƒ์„ฑ ๊ทœ์น™ ID + * @param childCount ์ž์‹ ๋…ธ๋“œ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun astBuilderValidationFailed(ruleId: Int, childCount: Int): ParserException = + ParserException( + errorCode = ErrorCode.AST_BUILDER_VALIDATION_FAILED, + message = "AST ๋นŒ๋” ๊ฒ€์ฆ ์‹คํŒจ: ๊ทœ์น™ $ruleId, ์ž์‹ ๊ฐœ์ˆ˜ $childCount" + ) + + /** + * ์ฃผ์–ด์ง„ ํ† ํฐ์ด ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ๊ฒ€์ฆ ๋Œ€์ƒ ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun notArithmeticOperator( + tokenType: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.NOT_ARITHMETIC_OPERATOR, + message = "์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค: $tokenType" + ) + + /** + * ์ง€์›๋˜์ง€ ์•Š๋Š” ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ์—ฐ์‚ฐ์ž ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedArithmeticOperator( + tokenType: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.UNSUPPORTED_ARITHMETIC_OPERATOR, + message = "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž: $tokenType" + ) + + /** + * ์ฃผ์–ด์ง„ ํ† ํฐ์ด ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ๊ฒ€์ฆ ๋Œ€์ƒ ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun notLogicalOperator( + tokenType: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.NOT_LOGICAL_OPERATOR, + message = "๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค: $tokenType" + ) + + /** + * ์ง€์›๋˜์ง€ ์•Š๋Š” ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ์—ฐ์‚ฐ์ž ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedLogicalOperator( + tokenType: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.UNSUPPORTED_LOGICAL_OPERATOR, + message = "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž: $tokenType" + ) + + /** + * ์ฃผ์–ด์ง„ ํ† ํฐ์ด ๋น„๊ต ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ๊ฒ€์ฆ ๋Œ€์ƒ ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun notComparisonOperator( + tokenType: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.NOT_COMPARISON_OPERATOR, + message = "๋น„๊ต ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค: $tokenType" + ) + + /** + * ์ง€์›๋˜์ง€ ์•Š๋Š” ๋น„๊ต ์—ฐ์‚ฐ์ž์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ์—ฐ์‚ฐ์ž ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedComparisonOperator( + tokenType: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.UNSUPPORTED_COMPARISON_OPERATOR, + message = "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋น„๊ต ์—ฐ์‚ฐ์ž: $tokenType" + ) + + /** + * ์ฃผ์–ด์ง„ ํ† ํฐ์ด ๋ฆฌํ„ฐ๋Ÿด์ด ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ๊ฒ€์ฆ ๋Œ€์ƒ ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun notLiteralToken( + tokenType: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.NOT_LITERAL_TOKEN, + message = "๋ฆฌํ„ฐ๋Ÿด ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค: $tokenType" + ) + + /** + * ์ง€์›๋˜์ง€ ์•Š๋Š” ๋ฆฌํ„ฐ๋Ÿด ํƒ€์ž…์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ๋ฆฌํ„ฐ๋Ÿด ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedLiteralType( + tokenType: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.UNSUPPORTED_LITERAL_TYPE, + message = "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ฆฌํ„ฐ๋Ÿด ํƒ€์ž…: $tokenType" + ) + + /** + * ๋นŒ๋” ์ด๋ฆ„์ด ๊ณต๋ฐฑ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋นŒ๋” ์ด๋ฆ„ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun builderNameBlank(name: String): ParserException = + ParserException( + errorCode = ErrorCode.BUILDER_NAME_BLANK, + message = "๋นŒ๋” ์ด๋ฆ„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด์ด ๊ณต๋ฐฑ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun operatorBlank(operator: String): ParserException = + ParserException( + errorCode = ErrorCode.OPERATOR_BLANK, + message = "์—ฐ์‚ฐ์ž๋Š” ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด์ด ํ—ˆ์šฉ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @param maxLength ํ—ˆ์šฉ ์ตœ๋Œ€ ๊ธธ์ด(๊ธฐ๋ณธ 3) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun operatorTooLong(operator: String, maxLength: Int = 3): ParserException = + ParserException( + errorCode = ErrorCode.OPERATOR_TOO_LONG, + message = "์—ฐ์‚ฐ์ž ๊ธธ์ด๊ฐ€ ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค: $operator" + ) + + /** + * ์ปค๋„ ์•„์ดํ…œ์˜ ์  ์œ„์น˜๊ฐ€ 0 ์ดํ•˜์ธ๋ฐ ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™(-1)์ด ์•„๋‹Œ ๊ฒฝ์šฐ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param dotPos ์ ์˜ ํ˜„์žฌ ์œ„์น˜ + * @param productionId ๋Œ€์ƒ ์ƒ์‚ฐ ๊ทœ์น™ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun kernelDotPositionInvalid(dotPos: Int, productionId: Int): ParserException = + ParserException( + errorCode = ErrorCode.KERNEL_DOT_POSITION_INVALID, + message = "์ปค๋„ ์•„์ดํ…œ์˜ ์  ์œ„์น˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค (ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™ ์ œ์™ธ): $dotPos (prodId=$productionId)" + ) + + /** + * ์‹œ์ž‘ ์•„์ดํ…œ์ด ํ™•์žฅ(augmented) ์ƒ์‚ฐ ๊ทœ์น™(-1)์„ ์‚ฌ์šฉํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actualId ์‹ค์ œ ์‚ฌ์šฉ๋œ ์‹œ์ž‘ ์ƒ์‚ฐ ๊ทœ์น™ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun startItemMustUseAugmented(actualId: Int): ParserException = + ParserException( + errorCode = ErrorCode.START_ITEM_MUST_USE_AUGMENTED, + message = "์‹œ์ž‘ ์•„์ดํ…œ์€ ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $actualId" + ) + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ์šฐ๋ณ€ ๊ธธ์ด๊ฐ€ ํ—ˆ์šฉ ์ตœ๋Œ€์น˜๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param length ์‹ค์ œ ์šฐ๋ณ€ ๊ธธ์ด + * @param maxLength ํ—ˆ์šฉ ์ตœ๋Œ€ ๊ธธ์ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionLengthExceedsLimit(length: Int, maxLength: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_LENGTH_EXCEEDS_LIMIT, + message = "์ƒ์‚ฐ ๊ทœ์น™์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $length > $maxLength" + ) + + /** + * ์ „๋ฐฉํƒ์ƒ‰(lookahead) ์‹ฌ๋ณผ์˜ ๊ฐœ์ˆ˜๊ฐ€ ํ—ˆ์šฉ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param size ์‹ค์ œ ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ ๊ฐœ์ˆ˜ + * @param maxSize ํ—ˆ์šฉ ์ตœ๋Œ€ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun lookaheadSizeExceedsLimit(size: Int, maxSize: Int): ParserException = + ParserException( + errorCode = ErrorCode.LOOKAHEAD_SIZE_EXCEEDS_LIMIT, + message = "์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ์ด ์ตœ๋Œ€ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $size > $maxSize" + ) + + /** + * ๋ฌธ์ž์—ด ์ž…๋ ฅ์ด ๋น„์—ˆ๊ฑฐ๋‚˜ ๊ณต๋ฐฑ๋ฟ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์ž…๋ ฅ ํŒŒ๋ผ๋ฏธํ„ฐ ์ด๋ฆ„(๋กœ๊ทธ ์‹๋ณ„์šฉ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun inputBlank(name: String = "input"): ParserException = + ParserException( + errorCode = ErrorCode.INPUT_BLANK, + message = "์ž…๋ ฅ์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค ($name)" + ) + + /** + * Shift ์•ก์…˜์„ ๊ตฌ์„ฑํ•  ๋•Œ ์ƒํƒœ ๋ฒˆํ˜ธ๊ฐ€ ๋ˆ„๋ฝ๋œ ๊ฒฝ์šฐ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun shiftStateRequired(): ParserException = + ParserException( + errorCode = ErrorCode.ACTION_SHIFT_STATE_REQUIRED, + message = "Shift ์•ก์…˜์—๋Š” ์ƒํƒœ ๋ฒˆํ˜ธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" + ) + + /** + * Reduce ์•ก์…˜์„ ๊ตฌ์„ฑํ•  ๋•Œ ์ƒ์‚ฐ ๊ทœ์น™์ด ๋ˆ„๋ฝ๋œ ๊ฒฝ์šฐ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun reduceProductionRequired(): ParserException = + ParserException( + errorCode = ErrorCode.ACTION_REDUCE_PRODUCTION_REQUIRED, + message = "Reduce ์•ก์…˜์—๋Š” ์ƒ์‚ฐ ๊ทœ์น™์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" + ) + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์•ก์…˜ ํƒ€์ž…์ด ์ง€์ •๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actionType ์•ก์…˜ ํƒ€์ž…(๋ฌธ์ž์—ด ๋˜๋Š” enum ๋“ฑ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedActionType(actionType: Any): ParserException = + ParserException( + errorCode = ErrorCode.UNSUPPORTED_ACTION_TYPE, + message = "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์•ก์…˜ ํƒ€์ž…: $actionType" + ) + + /** + * ์ˆ˜๋ฝ(accepting) ์ƒํƒœ๊ฐ€ ์™„์„ฑ๋œ ์•„์ดํ…œ๋งŒ ํฌํ•จํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun acceptingStateItemsNotComplete(): ParserException = + ParserException( + errorCode = ErrorCode.ACCEPTING_STATE_ITEMS_NOT_COMPLETE, + message = "์ˆ˜๋ฝ ์ƒํƒœ๋Š” ์™„์„ฑ๋œ ์•„์ดํ…œ๋“ค๋งŒ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + + /** + * ๋‹ค์Œ ์ƒํƒœ ID๊ฐ€ ์ „์ฒด ์ƒํƒœ ์ตœ๋Œ€ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nextStateId ๋‹ค์Œ์œผ๋กœ ๋ถ€์—ฌํ•˜๋ ค๋Š” ์ƒํƒœ ID + * @param maxCount ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์ƒํƒœ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun stateCountExceedsLimit(nextStateId: Int, maxCount: Int): ParserException = + ParserException( + errorCode = ErrorCode.STATE_COUNT_EXCEEDS_LIMIT, + message = "์ƒํƒœ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $nextStateId >= $maxCount" + ) + + /** + * ์ƒํƒœ์— ํฌํ•จ๋œ ์•„์ดํ…œ ๊ฐœ์ˆ˜๊ฐ€ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ•  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param count ์‹ค์ œ ์•„์ดํ…œ ๊ฐœ์ˆ˜ + * @param maxCount ํ—ˆ์šฉ ์ตœ๋Œ€ ์•„์ดํ…œ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun itemsPerStateExceedsLimit(count: Int, maxCount: Int): ParserException = + ParserException( + errorCode = ErrorCode.ITEMS_PER_STATE_EXCEEDS_LIMIT, + message = "์ƒํƒœ์˜ ์•„์ดํ…œ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $count > $maxCount" + ) + + /** + * ์•ก์…˜ ํ…Œ์ด๋ธ”์— ๋น„ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ๋“ค์–ด๊ฐ”์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ๋ฌธ์ œ๊ฐ€ ๋œ ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun actionTableContainsNonTerminal( + symbol: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.ACTION_TABLE_CONTAINS_NON_TERMINAL, + message = "์•ก์…˜ ํ…Œ์ด๋ธ”์— ๋น„ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์žˆ์Šต๋‹ˆ๋‹ค: $symbol" + ) + + /** + * Goto ํ…Œ์ด๋ธ”์— ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ๋“ค์–ด๊ฐ”์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ๋ฌธ์ œ๊ฐ€ ๋œ ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun gotoTableContainsTerminal( + symbol: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.GOTO_TABLE_CONTAINS_TERMINAL, + message = "Goto ํ…Œ์ด๋ธ”์— ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์žˆ์Šต๋‹ˆ๋‹ค: $symbol" + ) + + /** + * ๋ชฉํ‘œ ์ƒํƒœ ID๊ฐ€ ์Œ์ˆ˜์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param targetState ๋ชฉํ‘œ ์ƒํƒœ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun targetStateNegative(targetState: Int): ParserException = + ParserException( + errorCode = ErrorCode.TARGET_STATE_NEGATIVE, + message = "๋ชฉํ‘œ ์ƒํƒœ ID๊ฐ€ ์Œ์ˆ˜์ž…๋‹ˆ๋‹ค: $targetState" + ) + + /** + * ํ•œ ์ƒํƒœ์— ์ •์˜๋œ ์ „์ด ๊ฐœ์ˆ˜๊ฐ€ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param count ์‹ค์ œ ์ „์ด ๊ฐœ์ˆ˜ + * @param maxCount ํ—ˆ์šฉ ์ตœ๋Œ€ ์ „์ด ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun transitionsPerStateExceedsLimit(count: Int, maxCount: Int): ParserException = + ParserException( + errorCode = ErrorCode.TRANSITIONS_PER_STATE_EXCEEDS_LIMIT, + message = "์ „์ด ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $count > $maxCount" + ) + + /** + * ์ฃผ์–ด์ง„ ์‹ฌ๋ณผ์ด ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ๊ฒ€์ฆ ๋Œ€์ƒ ์‹ฌ๋ณผ(ํ† ํฐ ํƒ€์ž…) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun notOperatorSymbol( + operator: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.NOT_OPERATOR_SYMBOL, + message = "์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ์ด ์•„๋‹™๋‹ˆ๋‹ค: $operator" + ) + + /** + * BNF ๊ทœ์น™ ๋ฌธ์ž์—ด์ด ์œ ํšจํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param bnfRule ์›๋ณธ BNF ๊ทœ์น™ ๋ฌธ์ž์—ด + * @param partsCount ํŒŒ์‹ฑ๋œ ํŒŒํŠธ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun invalidBnfFormat(bnfRule: String, partsCount: Int): ParserException = + ParserException( + errorCode = ErrorCode.INVALID_BNF_FORMAT, + message = "์ž˜๋ชป๋œ BNF ํ˜•์‹: $bnfRule (parts=$partsCount)" + ) + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ์ด ๊ฐœ์ˆ˜๊ฐ€ ํ—ˆ์šฉ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param count ์‹ค์ œ ์ƒ์‚ฐ ๊ทœ์น™ ๊ฐœ์ˆ˜ + * @param maxCount ํ—ˆ์šฉ ์ตœ๋Œ€ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionCountExceedsLimit(count: Int, maxCount: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_COUNT_EXCEEDS_LIMIT, + message = "์ƒ์‚ฐ ๊ทœ์น™ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $count > $maxCount" + ) + + /** + * ์•Œ ์ˆ˜ ์—†๋Š” ํ† ํฐ ๋ฌธ์ž์—ด์ด ์ฃผ์–ด์กŒ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenString ๋ฏธํ™•์ธ ํ† ํฐ ์›๋ณธ ๋ฌธ์ž์—ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unknownTokenType(tokenString: String): ParserException = + ParserException( + errorCode = ErrorCode.UNKNOWN_TOKEN_TYPE, + message = "์•Œ ์ˆ˜ ์—†๋Š” ํ† ํฐ ํƒ€์ž…: $tokenString" + ) + + /** + * ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์˜ ์—ฐ์‚ฐ์ž์™€ ์‹ค์ œ ํ† ํฐ ํƒ€์ž…์ด ์ผ์น˜ํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param expected ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์— ๋ช…์‹œ๋œ ์—ฐ์‚ฐ์ž ํ† ํฐ ํƒ€์ž… + * @param actual ์‹ค์ œ ๋น„๊ต ๋Œ€์ƒ ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun associativityOperatorMismatch( + expected: hs.kr.entrydsm.domain.lexer.entities.TokenType, + actual: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.ASSOCIATIVITY_OPERATOR_MISMATCH, + message = "๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์˜ ์—ฐ์‚ฐ์ž์™€ ํ† ํฐ ํƒ€์ž…์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $expected != $actual" + ) + + /** + * ์ขŒ์žฌ๊ท€๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nonTerminal ์ขŒ์žฌ๊ท€๊ฐ€ ๊ฐ์ง€๋œ ๋…ผํ„ฐ๋ฏธ๋„ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun leftRecursionDetected( + nonTerminal: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.LEFT_RECURSION_DETECTED, + message = "์ขŒ์žฌ๊ท€๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค: $nonTerminal" + ) + + /** + * ๋„๋‹ฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ๋…ผํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ์ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param unreachable ๋„๋‹ฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ๋…ผํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unreachableNonTerminals( + unreachable: Set + ): ParserException = + ParserException( + errorCode = ErrorCode.UNREACHABLE_NON_TERMINALS, + message = "๋„๋‹ฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ๋…ผํ„ฐ๋ฏธ๋„๋“ค: $unreachable" + ) + + /** + * ์ •์˜๋˜์ง€ ์•Š์€ ๋…ผํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ์ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param undefined ์ •์˜๋˜์ง€ ์•Š์€ ๋…ผํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun undefinedNonTerminals( + undefined: Set + ): ParserException = + ParserException( + errorCode = ErrorCode.UNDEFINED_NON_TERMINALS, + message = "์ •์˜๋˜์ง€ ์•Š์€ ๋…ผํ„ฐ๋ฏธ๋„๋“ค: $undefined" + ) + + /** + * ๋ชจํ˜ธํ•œ ๋ฌธ๋ฒ• ๊ทœ์น™(์ค‘๋ณต ํ˜•ํƒœ)์ด ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ๋ณ€ ๋…ผํ„ฐ๋ฏธ๋„ + * @param duplicates ์ค‘๋ณต๋œ ๊ทœ์น™ ํ‚ค ์ง‘ํ•ฉ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun ambiguousGrammarRule( + left: hs.kr.entrydsm.domain.lexer.entities.TokenType, + duplicates: Collection<*> + ): ParserException = + ParserException( + errorCode = ErrorCode.AMBIGUOUS_GRAMMAR_RULE, + message = "๋ชจํ˜ธํ•œ ๋ฌธ๋ฒ• ๊ทœ์น™ ๊ฐ์ง€: $left -> $duplicates" + ) + + /** + * ๋ฌธ๋ฒ• ๋‚ด ์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param start ์ˆœํ™˜ ์‹œ์ž‘ ์ง€์ ์˜ ๋…ผํ„ฐ๋ฏธ๋„ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun cyclicGrammarReference( + start: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.CYCLIC_GRAMMAR_REFERENCE, + message = "์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค: $start" + ) + + /** + * ์ƒ์‚ฐ ๊ทœ์น™ ์ด ๊ฐœ์ˆ˜๊ฐ€ ์ตœ์†Œ ๊ฐœ์ˆ˜๋ณด๋‹ค ์ ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param count ์‹ค์ œ ๊ฐœ์ˆ˜ + * @param minCount ์ตœ์†Œ ์š”๊ตฌ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionCountBelowMin(count: Int, minCount: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_COUNT_BELOW_MIN, + message = "์ƒ์‚ฐ ๊ทœ์น™์ด ์ตœ์†Œ ๊ฐœ์ˆ˜๋ณด๋‹ค ์ ์Šต๋‹ˆ๋‹ค: $count < $minCount" + ) + + /** + * ์‹œ์ž‘ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ์— ํฌํ•จ๋˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startSymbol ์‹œ์ž‘ ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun startSymbolNotInNonTerminals( + startSymbol: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.START_SYMBOL_NOT_IN_NON_TERMINALS, + message = "์‹œ์ž‘ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์— ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: $startSymbol" + ) + + /** + * ํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ๊ณผ ๋…ผํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ์ด ๊ฒน์น  ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param overlap ๊ฒน์น˜๋Š” ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun terminalsAndNonTerminalsOverlap( + overlap: Set + ): ParserException = + ParserException( + errorCode = ErrorCode.TERMINALS_NON_TERMINALS_OVERLAP, + message = "ํ„ฐ๋ฏธ๋„๊ณผ ๋…ผํ„ฐ๋ฏธ๋„์ด ๊ฒน์นฉ๋‹ˆ๋‹ค: $overlap" + ) + + /** + * ์ค‘๋ณต๋œ ์ƒ์‚ฐ ๊ทœ์น™์ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param duplicates ์ค‘๋ณต ๊ทœ์น™ ํ‚ค ์ง‘ํ•ฉ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun duplicateProductions( + duplicates: Collection<*> + ): ParserException = + ParserException( + errorCode = ErrorCode.DUPLICATE_PRODUCTIONS, + message = "์ค‘๋ณต๋œ ์ƒ์‚ฐ ๊ทœ์น™๋“ค: $duplicates" + ) + + /** + * ์ƒ์‚ฐ ๊ทœ์น™ ID๊ฐ€ ์Œ์ˆ˜์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionIdNegative(id: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_ID_NEGATIVE, + message = "์ƒ์‚ฐ ๊ทœ์น™ ID๊ฐ€ ์Œ์ˆ˜์ž…๋‹ˆ๋‹ค: $id" + ) + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์— ์•Œ ์ˆ˜ ์—†๋Š” ์‹ฌ๋ณผ์ด ์‚ฌ์šฉ๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ์•Œ ์ˆ˜ ์—†๋Š” ์‹ฌ๋ณผ + * @param productionId ๋Œ€์ƒ ์ƒ์‚ฐ ๊ทœ์น™ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unknownSymbolInProduction( + symbol: hs.kr.entrydsm.domain.lexer.entities.TokenType, + productionId: Int + ): ParserException = + ParserException( + errorCode = ErrorCode.UNKNOWN_SYMBOL_IN_PRODUCTION, + message = "์•Œ ์ˆ˜ ์—†๋Š” ์‹ฌ๋ณผ์ž…๋‹ˆ๋‹ค: $symbol in production $productionId" + ) + + /** + * ๋‘ ์ƒํƒœ๊ฐ€ LALR ๋ณ‘ํ•ฉ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param leftId ์ฒซ ๋ฒˆ์งธ ์ƒํƒœ ID + * @param rightId ๋‘ ๋ฒˆ์งธ ์ƒํƒœ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun lalrStatesCannotMerge(leftId: Int, rightId: Int): ParserException = + ParserException( + errorCode = ErrorCode.LALR_STATES_CANNOT_MERGE, + message = "์ƒํƒœ $rightId ์™€ $rightId ๋Š” LALR ๋ณ‘ํ•ฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค" + ) + + /** + * ๋ณ‘ํ•ฉํ•˜๋ ค๋Š” ์ƒํƒœ ์ปฌ๋ ‰์…˜์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun noStatesToMerge(): ParserException = + ParserException( + errorCode = ErrorCode.NO_STATES_TO_MERGE, + message = "๋ณ‘ํ•ฉํ•  ์ƒํƒœ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ์‹œ๋„ ํšŸ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param attempts ํ˜„์žฌ๊นŒ์ง€์˜ ์‹œ๋„ ํšŸ์ˆ˜ + * @param maxAttempts ํ—ˆ์šฉ๋˜๋Š” ์ตœ๋Œ€ ์‹œ๋„ ํšŸ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun conflictResolutionExceeded(attempts: Int, maxAttempts: Int): ParserException = + ParserException( + errorCode = ErrorCode.CONFLICT_RESOLUTION_EXCEEDED, + message = "์ถฉ๋Œ ํ•ด๊ฒฐ์ด ์ตœ๋Œ€ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $attempts > $maxAttempts" + ) + + /** + * Shift/Reduce ์ถฉ๋Œ์ด ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param conflictSymbol ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์‹ฌ๋ณผ + * @param stateId ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์ƒํƒœ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun shiftReduceConflict( + conflictSymbol: hs.kr.entrydsm.domain.lexer.entities.TokenType, + stateId: Int + ): ParserException = + ParserException( + errorCode = ErrorCode.SHIFT_REDUCE_CONFLICT, + message = "Shift/Reduce ์ถฉ๋Œ: $conflictSymbol in state $stateId" + ) + + /** + * Reduce/Reduce ์ถฉ๋Œ์„ ์ •์ฑ…์ƒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param conflictSymbol ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์‹ฌ๋ณผ + * @param stateId ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์ƒํƒœ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun reduceReduceConflictUnresolvable( + conflictSymbol: hs.kr.entrydsm.domain.lexer.entities.TokenType, + stateId: Int + ): ParserException = + ParserException( + errorCode = ErrorCode.REDUCE_REDUCE_CONFLICT_UNRESOLVABLE, + message = "Reduce/Reduce ์ถฉ๋Œ ํ•ด๊ฒฐ ๋ถˆ๊ฐ€: $conflictSymbol in state $stateId" + ) + + /** + * ๋น„๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž์— ๋Œ€ํ•œ ์ถฉ๋Œ์ด ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param conflictSymbol ๋น„๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun nonAssociativeOperatorConflict( + conflictSymbol: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.NON_ASSOCIATIVE_OPERATOR_CONFLICT, + message = "๋น„๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž ์ถฉ๋Œ: $conflictSymbol" + ) + + /** + * ์ƒ์‚ฐ ๊ทœ์น™ ID๋Š” Reduce ์•ก์…˜์—๋งŒ ํ—ˆ์šฉ๋  ๋•Œ, ๋‹ค๋ฅธ ์•ก์…˜์—์„œ ์ ‘๊ทผํ•œ ๊ฒฝ์šฐ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actionType ํ˜„์žฌ ์•ก์…˜ ํƒ€์ž…(Shift/Reduce/Accept ๋“ฑ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionIdOnlyForReduce(): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_ID_ONLY_FOR_REDUCE, + message = "Only Reduce actions have production IDs" + ) + + /** + * FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ์ด ์ˆ˜๋ ดํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param iterations ์ˆ˜ํ–‰ํ•œ ๋ฐ˜๋ณต ํšŸ์ˆ˜(์„ ํƒ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun firstSetNotConverging(iterations: Int? = null): ParserException = + ParserException( + errorCode = ErrorCode.FIRST_SET_NOT_CONVERGING, + message = buildString { + append("FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ์ด ์ˆ˜๋ ดํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") + if (iterations != null) append(" (iterations=$iterations)") + } + ) + + /** + * FOLLOW ์ง‘ํ•ฉ ๊ณ„์‚ฐ์ด ์ˆ˜๋ ดํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param iterations ์ˆ˜ํ–‰ํ•œ ๋ฐ˜๋ณต ํšŸ์ˆ˜(์„ ํƒ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun followSetNotConverging(iterations: Int? = null): ParserException = + ParserException( + errorCode = ErrorCode.FOLLOW_SET_NOT_CONVERGING, + message = buildString { + append("FOLLOW ์ง‘ํ•ฉ ๊ณ„์‚ฐ์ด ์ˆ˜๋ ดํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค") + if (iterations != null) append(" (iterations=$iterations)") + } + ) + + /** + * ํ™•์žฅ(augmented) ์ƒ์‚ฐ ๊ทœ์น™์„ ์ฐพ์ง€ ๋ชปํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun augmentedProductionNotFound(): ParserException = + ParserException( + errorCode = ErrorCode.AUGMENTED_PRODUCTION_NOT_FOUND, + message = "ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * Reduce/Reduce ๋˜๋Š” Shift/Reduce ์ถฉ๋Œ์ด ์ผ๋ฐ˜์ ์œผ๋กœ ๊ฐ์ง€๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param lookahead ์ถฉ๋Œ์„ ์œ ๋ฐœํ•œ lookahead ์‹ฌ๋ณผ + * @param stateId ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์ƒํƒœ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun lrConflictDetected( + lookahead: hs.kr.entrydsm.domain.lexer.entities.TokenType, + stateId: Int + ): ParserException = + ParserException( + errorCode = ErrorCode.LR_CONFLICT_DETECTED, + message = "Reduce/Reduce ๋˜๋Š” Shift/Reduce ์ถฉ๋Œ: $lookahead in state $stateId" + ) + + /** + * ์ƒํƒœ ์ˆ˜๊ฐ€ 0 ์ดํ•˜์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param numStates ์ƒํƒœ ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun numStatesNotPositive(numStates: Int): ParserException = + ParserException( + errorCode = ErrorCode.NUM_STATES_NOT_POSITIVE, + message = "์ƒํƒœ ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค (numStates=$numStates)" + ) + + /** + * ํ„ฐ๋ฏธ๋„ ์ˆ˜๊ฐ€ 0 ์ดํ•˜์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param numTerminals ํ„ฐ๋ฏธ๋„ ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun numTerminalsNotPositive(numTerminals: Int): ParserException = + ParserException( + errorCode = ErrorCode.NUM_TERMINALS_NOT_POSITIVE, + message = "ํ„ฐ๋ฏธ๋„ ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค (numTerminals=$numTerminals)" + ) + + /** + * ๋…ผํ„ฐ๋ฏธ๋„ ์ˆ˜๊ฐ€ 0 ์ดํ•˜์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param numNonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun numNonTerminalsNotPositive(numNonTerminals: Int): ParserException = + ParserException( + errorCode = ErrorCode.NUM_NON_TERMINALS_NOT_POSITIVE, + message = "๋…ผํ„ฐ๋ฏธ๋„ ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค (numNonTerminals=$numNonTerminals)" + ) + + /** + * ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๊ฐ€ ์–‘์ˆ˜๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxDepth ์š”์ฒญ๋œ ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun maxParsingDepthNotPositive(maxDepth: Int): ParserException = + ParserException( + errorCode = ErrorCode.MAX_PARSING_DEPTH_NOT_POSITIVE, + message = "์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค: $maxDepth" + ) + + /** + * ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๊ฐ€ ์Šคํƒ ํ•œ๊ณ„๋ฅผ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxDepth ์š”์ฒญ๋œ ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด + * @param limit ํ—ˆ์šฉ ํ•œ๊ณ„(์Šคํƒ ๊นŠ์ด) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun maxParsingDepthExceedsLimit(maxDepth: Int, limit: Int): ParserException = + ParserException( + errorCode = ErrorCode.MAX_PARSING_DEPTH_EXCEEDS_LIMIT, + message = "์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๊ฐ€ ํ•œ๊ณ„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $maxDepth > $limit" + ) + + /** + * ํ† ํฐ ๊ฐœ์ˆ˜๊ฐ€ ํ—ˆ์šฉ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param count ์‹ค์ œ ํ† ํฐ ๊ฐœ์ˆ˜ + * @param limit ํ—ˆ์šฉ ์ตœ๋Œ€ ํ† ํฐ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun tokenCountExceedsLimit(count: Int, limit: Int): ParserException = + ParserException( + errorCode = ErrorCode.TOKEN_COUNT_EXCEEDS_LIMIT, + message = "ํ† ํฐ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $count > $limit" + ) + + /** + * ์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ๊ฐ€ ์–‘์ˆ˜๊ฐ€ ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxSize ์„ค์ •ํ•˜๋ ค๋Š” ์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun maxStackSizeNotPositive(maxSize: Int): ParserException = + ParserException( + errorCode = ErrorCode.MAX_STACK_SIZE_NOT_POSITIVE, + message = "์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค: $maxSize" + ) + + /** + * ํ† ํฐ ์‹œํ€€์Šค ๊ธธ์ด๊ฐ€ ํ—ˆ์šฉ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param count ์‹ค์ œ ํ† ํฐ ๊ฐœ์ˆ˜ + * @param limit ํ—ˆ์šฉ ์ตœ๋Œ€ ํ† ํฐ ์‹œํ€€์Šค ๊ธธ์ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun tokenSequenceExceedsLimit(count: Int, limit: Int): ParserException = + ParserException( + errorCode = ErrorCode.TOKEN_SEQUENCE_EXCEEDS_LIMIT, + message = "ํ† ํฐ ์‹œํ€€์Šค๊ฐ€ ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $count > $limit" + ) + + /** + * ์ค‘์ฒฉ ๊นŠ์ด๊ฐ€ ํ—ˆ์šฉ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param depth ์‹ค์ œ ์ค‘์ฒฉ ๊นŠ์ด + * @param limit ํ—ˆ์šฉ ์ตœ๋Œ€ ์ค‘์ฒฉ ๊นŠ์ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun nestingDepthExceedsLimit(depth: Int, limit: Int): ParserException = + ParserException( + errorCode = ErrorCode.NESTING_DEPTH_EXCEEDS_LIMIT, + message = "์ค‘์ฒฉ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $depth > $limit" + ) + + /** + * ํ‘œํ˜„์‹์˜ ๋ณต์žก๋„๊ฐ€ ํ—ˆ์šฉ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param complexity ์‹ค์ œ ๋ณต์žก๋„ ๊ฐ’ + * @param limit ํ—ˆ์šฉ ์ตœ๋Œ€ ๋ณต์žก๋„ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun expressionComplexityExceedsLimit(complexity: Number, limit: Number): ParserException = + ParserException( + errorCode = ErrorCode.EXPRESSION_COMPLEXITY_EXCEEDS_LIMIT, + message = "ํ‘œํ˜„์‹ ๋ณต์žก๋„๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $complexity > $limit" + ) + + /** + * ์—ฐ์‚ฐ์ž ์ •์˜ ์ง€์ ์—์„œ ์ „๋‹ฌ๋œ ํ† ํฐ์ด ์—ฐ์‚ฐ์ž(๋˜๋Š” ํ—ˆ์šฉ๋œ ํ„ฐ๋ฏธ๋„) ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜์ง€ ์•Š์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ๊ฒ€์ฆ ๋Œ€์ƒ ํ† ํฐ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun operatorTokenRequired( + operator: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.OPERATOR_TOKEN_REQUIRED, + message = "์—ฐ์‚ฐ์ž ํ† ํฐ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $operator" + ) + + /** + * ์šฐ์„ ์ˆœ์œ„ ๊ฐ’์ด 0 ๋ฏธ๋งŒ์œผ๋กœ ์„ค์ •๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param precedence ์„ค์ •๋œ ์šฐ์„ ์ˆœ์œ„ ๊ฐ’ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun precedenceNegative(precedence: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRECEDENCE_NEGATIVE, + message = "์šฐ์„ ์ˆœ์œ„๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $precedence" + ) + + /** + * ๊ฒฐํ•ฉ์„ฑ(associativity) ๊ทœ์น™์— ์‚ฌ์šฉ๋œ ์‹ฌ๋ณผ์ด ์•Œ ์ˆ˜ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ํŒŒ์‹ฑ๋œ ๊ฒฐํ•ฉ์„ฑ ์‹ฌ๋ณผ(์˜ˆ: "left", "right", "nonassoc"๊ฐ€ ์•„๋‹Œ ๊ฐ’) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unknownAssociativitySymbol(symbol: String): ParserException = + ParserException( + errorCode = ErrorCode.UNKNOWN_ASSOCIATIVITY_SYMBOL, + message = "์•Œ ์ˆ˜ ์—†๋Š” ๊ฒฐํ•ฉ์„ฑ ์‹ฌ๋ณผ: $symbol" + ) + + /** + * ์ƒ์„ฑ ๊ทœ์น™ ID๊ฐ€ ์œ ํšจ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ๊ฒ€์‚ฌํ•œ ์ƒ์„ฑ ๊ทœ์น™ ID + * @param total ์ด ์ƒ์„ฑ ๊ทœ์น™ ๊ฐœ์ˆ˜ (์ƒํ•œ ๊ณ„์‚ฐ์— ์‚ฌ์šฉ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionIdOutOfRange(id: Int, total: Int): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_ID_OUT_OF_RANGE, + message = "์ƒ์„ฑ ๊ทœ์น™ ID๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค: $id, ๋ฒ”์œ„: 0-${total - 1}" + ) + + /** + * ์ฃผ์–ด์ง„ ์‹๋ณ„์ž/๋ฌธ์ž์—ด์— ํ•ด๋‹นํ•˜๋Š” ์ƒ์„ฑ ๊ทœ์น™์„ ์ฐพ์ง€ ๋ชปํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param rule ์‹๋ณ„์ž ๋˜๋Š” ์›๋ณธ ๋ฌธ์ž์—ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun productionNotFound(rule: Any): ParserException = + ParserException( + errorCode = ErrorCode.PRODUCTION_NOT_FOUND, + message = "์ƒ์„ฑ ๊ทœ์น™์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $rule" + ) + + /** + * ์ „๋‹ฌ๋œ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ด ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nonTerminal ๊ฒ€์‚ฌ ๋Œ€์ƒ ์‹ฌ๋ณผ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun symbolNotNonTerminal( + nonTerminal: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.SYMBOL_NOT_NON_TERMINAL, + message = "๋…ผํ„ฐ๋ฏธ๋„์ด ์•„๋‹™๋‹ˆ๋‹ค: $nonTerminal" + ) + + /** + * ๋ฌธ๋ฒ• ์ „์ฒด ์œ ํšจ์„ฑ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun grammarInvalid(): ParserException = + ParserException( + errorCode = ErrorCode.GRAMMAR_INVALID, + message = "๋ฌธ๋ฒ•์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + ) + + /** + * ์ƒํƒœ ID๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ๊ฒ€์ฆํ•œ ์ƒํƒœ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun stateIdNegative(state: Int): ParserException = + ParserException( + errorCode = ErrorCode.STATE_ID_NEGATIVE, + message = "์ƒํƒœ ID๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $state" + ) + + /** + * ํ˜„์žฌ ์•ก์…˜์ด Reduce ํƒ€์ž…์ด ์•„๋‹ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actionType ํ˜„์žฌ ์•ก์…˜ ํƒ€์ž…(์˜ˆ: Shift/Accept/Error ๋“ฑ) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun notReduceAction(actionType: Any): ParserException = + ParserException( + errorCode = ErrorCode.NOT_REDUCE_ACTION, + message = "Reduce ์•ก์…˜์ด ์•„๋‹™๋‹ˆ๋‹ค: $actionType" + ) + + /** + * ํ† ํฐ์˜ ๊ฐ’์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ๊ฐ’ ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ํ† ํฐ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun tokenValueEmpty( + token: hs.kr.entrydsm.domain.lexer.entities.Token + ): ParserException = + ParserException( + errorCode = ErrorCode.TOKEN_VALUE_EMPTY, + message = "ํ† ํฐ์˜ ๊ฐ’์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (type=${token.type})" + ) + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก์ด ๋น„์–ด ์žˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun argumentsEmpty(): ParserException = + ParserException( + errorCode = ErrorCode.ARGUMENTS_EMPTY, + message = "์ธ์ˆ˜ ๋ชฉ๋ก์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + + /** + * ์ธ์ˆ˜ ์ธ๋ฑ์Šค๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param index ์ ‘๊ทผํ•˜๋ ค๋Š” ์ธ๋ฑ์Šค + * @param size ์ธ์ˆ˜ ๋ชฉ๋ก์˜ ํฌ๊ธฐ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun argumentIndexOutOfRange(index: Int, size: Int): ParserException = + ParserException( + errorCode = ErrorCode.ARGUMENT_INDEX_OUT_OF_RANGE, + message = "์ธ์ˆ˜ ์ธ๋ฑ์Šค๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค: $index (size=$size)" + ) + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ฐ์ฒด ํƒ€์ž…์ด ์ „๋‹ฌ๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param typeName ์ง€์›๋˜์ง€ ์•Š๋Š” ํƒ€์ž…์˜ ๋‹จ์ˆœ ์ด๋ฆ„ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun unsupportedObjectType(typeName: String): ParserException = + ParserException( + errorCode = ErrorCode.UNSUPPORTED_OBJECT_TYPE, + message = "์ง€์›ํ•˜์ง€ ์•Š๋Š” ํƒ€์ž…์ž…๋‹ˆ๋‹ค: $typeName" + ) + + /** + * ์‹คํŒจํ•œ ParsingResult์— error ์ •๋ณด๊ฐ€ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun failedResultMissingError(): ParserException = + ParserException( + errorCode = ErrorCode.FAILED_RESULT_MISSING_ERROR, + message = "์‹คํŒจํ•œ ParsingResult๋Š” ๋ฐ˜๋“œ์‹œ error ์ •๋ณด๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + + /** + * ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„์ด 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param duration ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„(ms) + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun durationNegative(duration: Long): ParserException = + ParserException( + errorCode = ErrorCode.DURATION_NEGATIVE, + message = "๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $duration" + ) + + /** + * ํ† ํฐ ๊ฐœ์ˆ˜๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenCount ํ† ํฐ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun tokenCountNegative(tokenCount: Int): ParserException = + ParserException( + errorCode = ErrorCode.TOKEN_COUNT_NEGATIVE, + message = "ํ† ํฐ ๊ฐœ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $tokenCount" + ) + + /** + * ๋…ธ๋“œ ๊ฐœ์ˆ˜๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodeCount ๋…ธ๋“œ ๊ฐœ์ˆ˜ + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun nodeCountNegative(nodeCount: Int): ParserException = + ParserException( + errorCode = ErrorCode.NODE_COUNT_NEGATIVE, + message = "๋…ธ๋“œ ๊ฐœ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $nodeCount" + ) + + /** + * ์ตœ๋Œ€ ๊นŠ์ด๊ฐ€ 0 ๋ฏธ๋งŒ์ผ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxDepth ์ตœ๋Œ€ ๊นŠ์ด + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun maxDepthNegative(maxDepth: Int): ParserException = + ParserException( + errorCode = ErrorCode.MAX_DEPTH_NEGATIVE, + message = "์ตœ๋Œ€ ๊นŠ์ด๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค: $maxDepth" + ) + + /** + * ์„ฑ๊ณตํ•œ ParsingResult์— AST๊ฐ€ ์—†์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun successResultMissingAst(): ParserException = + ParserException( + errorCode = ErrorCode.SUCCESS_RESULT_MISSING_AST, + message = "์„ฑ๊ณตํ•œ ParsingResult๋Š” ๋ฐ˜๋“œ์‹œ AST๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + ) + + /** + * ์ƒํƒœ ์กฐํšŒ์— ์‹คํŒจํ–ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param stateId ์ฐพ์œผ๋ ค๋Š” ์ƒํƒœ ID + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun stateNotFound(stateId: Int): ParserException = + ParserException( + errorCode = ErrorCode.STATE_NOT_FOUND, + message = "์ƒํƒœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $stateId" + ) + + /** + * ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์•„๋‹Œ ๊ฐ’์ด ์ „๋‹ฌ๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param terminal ๊ฒ€์‚ฌ ๋Œ€์ƒ ์‹ฌ๋ณผ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun terminalSymbolRequired( + terminal: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.TERMINAL_SYMBOL_REQUIRED, + message = "ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์•„๋‹™๋‹ˆ๋‹ค: $terminal" + ) + + /** + * ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์•„๋‹Œ ๊ฐ’์ด ์ „๋‹ฌ๋˜์—ˆ์„ ๋•Œ์˜ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param nonTerminal ๊ฒ€์‚ฌ ๋Œ€์ƒ ์‹ฌ๋ณผ ํƒ€์ž… + * @return ParserException ์ธ์Šคํ„ด์Šค + */ + fun nonTerminalSymbolRequired( + nonTerminal: hs.kr.entrydsm.domain.lexer.entities.TokenType + ): ParserException = + ParserException( + errorCode = ErrorCode.NON_TERMINAL_SYMBOL_REQUIRED, + message = "๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์•„๋‹™๋‹ˆ๋‹ค: $nonTerminal" + ) + } + + /** + * Parser ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒํƒœ, ํ† ํฐ, ์ƒ์„ฑ ๊ทœ์น™ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋งต + */ + fun getParserInfo(): Map { + val info = mutableMapOf() + + state?.let { info["state"] = it } + if (expectedTokens.isNotEmpty()) { info["expectedTokens"] = expectedTokens } + actualToken?.let { info["actualToken"] = it } + production?.let { info["production"] = it } + + return info + } + + /** + * ํŒŒ์„œ ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ์ถ”๊ฐ€๋กœ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์„œ ์˜ค๋ฅ˜ ์ •๋ณด ๋งต + */ + fun getFullErrorInfo(): Map { + val parserInfo = getParserInfo() + + return parserInfo.mapValues { (_, value) -> + when (value) { + is List<*> -> value.joinToString(", ") + else -> value?.toString() ?: "" + } + } + } + + override fun toString(): String { + val parserDetails = getParserInfo() + return if (parserDetails.isNotEmpty()) { + "${super.toString()}, parser=${parserDetails}" + } else { + super.toString() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ASTBuilderFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ASTBuilderFactory.kt new file mode 100644 index 00000000..dcd6b7bc --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ASTBuilderFactory.kt @@ -0,0 +1,474 @@ +package hs.kr.entrydsm.domain.parser.factories + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity +import hs.kr.entrydsm.domain.ast.factory.ASTBuilders +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.domain.parser.exceptions.ParserException + +/** + * AST Builder ์ƒ์„ฑ์„ ๋‹ด๋‹นํ•˜๋Š” ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Factory ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๋‹ค์–‘ํ•œ AST ๋นŒ๋”์˜ ์ƒ์„ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ , + * ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ํ•„์š”ํ•œ AST ๊ตฌ์ถ• ์ „๋žต์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ์ƒ์‚ฐ ๊ทœ์น™์— ์ ํ•ฉํ•œ + * AST ๋นŒ๋”๋ฅผ ๋™์ ์œผ๋กœ ์ƒ์„ฑํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Factory( + context = "parser", + complexity = Complexity.NORMAL, + cache = true +) +class ASTBuilderContractFactory { + + companion object { + private val builderCache = mutableMapOf() + private const val MAX_CACHE_SIZE = 1000 + } + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ์ž AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @param precedence ์šฐ์„ ์ˆœ์œ„ (์˜ต์…˜) + * @param isLeftAssoc ์ขŒ๊ฒฐํ•ฉ ์—ฌ๋ถ€ (์˜ต์…˜) + * @return ์ดํ•ญ ์—ฐ์‚ฐ์ž AST ๋นŒ๋” + */ + fun createBinaryOperatorBuilder( + operator: String, + precedence: Int? = null, + isLeftAssoc: Boolean? = null + ): ASTBuilderContract { + val cacheKey = "binary:$operator:$precedence:$isLeftAssoc" + + return builderCache.getOrPut(cacheKey) { + validateOperator(operator) + ASTBuilders.createBinaryOp(operator) + } + } + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ์ž AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + * @param isPrefix ์ „์œ„ ์—ฐ์‚ฐ์ž ์—ฌ๋ถ€ + * @return ๋‹จํ•ญ ์—ฐ์‚ฐ์ž AST ๋นŒ๋” + */ + fun createUnaryOperatorBuilder( + operator: String, + isPrefix: Boolean = true + ): ASTBuilderContract { + val cacheKey = "unary:$operator:$isPrefix" + + return builderCache.getOrPut(cacheKey) { + validateOperator(operator) + ASTBuilders.createUnaryOp(operator) + } + } + + /** + * ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ์—ฐ์‚ฐ์ž ํ† ํฐ ํƒ€์ž… + * @return ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž AST ๋นŒ๋” + */ + fun createArithmeticBuilder(tokenType: TokenType): ASTBuilderContract { + if (!tokenType.isArithmeticOperator()) { + throw ParserException.notArithmeticOperator(tokenType) + } + + val operator = when (tokenType) { + TokenType.PLUS -> "+" + TokenType.MINUS -> "-" + TokenType.MULTIPLY -> "*" + TokenType.DIVIDE -> "/" + TokenType.MODULO -> "%" + TokenType.POWER -> "^" + else -> throw ParserException.unsupportedArithmeticOperator(tokenType) + } + + return createBinaryOperatorBuilder(operator) + } + + /** + * ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ์—ฐ์‚ฐ์ž ํ† ํฐ ํƒ€์ž… + * @return ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž AST ๋นŒ๋” + */ + fun createLogicalBuilder(tokenType: TokenType): ASTBuilderContract { + if (!tokenType.isLogicalOperator()) { + throw ParserException.notLogicalOperator(tokenType) + } + val operator = when (tokenType) { + TokenType.AND -> "&&" + TokenType.OR -> "||" + TokenType.NOT -> "!" + else -> throw ParserException.unsupportedLogicalOperator(tokenType) + } + + return if (tokenType == TokenType.NOT) { + createUnaryOperatorBuilder(operator) + } else { + createBinaryOperatorBuilder(operator) + } + } + + /** + * ๋น„๊ต ์—ฐ์‚ฐ์ž AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ์—ฐ์‚ฐ์ž ํ† ํฐ ํƒ€์ž… + * @return ๋น„๊ต ์—ฐ์‚ฐ์ž AST ๋นŒ๋” + */ + fun createComparisonBuilder(tokenType: TokenType): ASTBuilderContract { + if (!tokenType.isComparisonOperator()) { + throw ParserException.notComparisonOperator(tokenType) + } + + val operator = when (tokenType) { + TokenType.EQUAL -> "==" + TokenType.NOT_EQUAL -> "!=" + TokenType.LESS -> "<" + TokenType.LESS_EQUAL -> "<=" + TokenType.GREATER -> ">" + TokenType.GREATER_EQUAL -> ">=" + else -> throw ParserException.unsupportedComparisonOperator(tokenType) + } + + return createBinaryOperatorBuilder(operator) + } + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param hasArguments ์ธ์ˆ˜๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ + * @param minArgs ์ตœ์†Œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ + * @param maxArgs ์ตœ๋Œ€ ์ธ์ˆ˜ ๊ฐœ์ˆ˜ (null์ด๋ฉด ์ œํ•œ ์—†์Œ) + * @return ํ•จ์ˆ˜ ํ˜ธ์ถœ AST ๋นŒ๋” + */ + fun createFunctionCallBuilder( + hasArguments: Boolean = true, + minArgs: Int = 0, + maxArgs: Int? = null + ): ASTBuilderContract { + val cacheKey = "function:$hasArguments:$minArgs:$maxArgs" + + return builderCache.getOrPut(cacheKey) { + if (hasArguments) { + ASTBuilders.FunctionCall + } else { + ASTBuilders.FunctionCallEmpty + } + } + } + + /** + * ์กฐ๊ฑด๋ถ€ ํ‘œํ˜„์‹ AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param isShortCircuit ๋‹จ์ถ• ํ‰๊ฐ€ ์—ฌ๋ถ€ + * @return ์กฐ๊ฑด๋ถ€ ํ‘œํ˜„์‹ AST ๋นŒ๋” + */ + fun createConditionalBuilder(isShortCircuit: Boolean = true): ASTBuilderContract { + val cacheKey = "conditional:$isShortCircuit" + + return builderCache.getOrPut(cacheKey) { + ASTBuilders.If + } + } + + /** + * ๋ฆฌํ„ฐ๋Ÿด ๊ฐ’ AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ๋ฆฌํ„ฐ๋Ÿด ํ† ํฐ ํƒ€์ž… + * @return ๋ฆฌํ„ฐ๋Ÿด AST ๋นŒ๋” + */ + fun createLiteralBuilder(tokenType: TokenType): ASTBuilderContract { + if (!tokenType.isLiteral) { + throw ParserException.notLiteralToken(tokenType) + } + + val cacheKey = "literal:$tokenType" + + return builderCache.getOrPut(cacheKey) { + when (tokenType) { + TokenType.NUMBER -> ASTBuilders.Number + TokenType.TRUE -> ASTBuilders.BooleanTrue + TokenType.FALSE -> ASTBuilders.BooleanFalse + TokenType.IDENTIFIER, TokenType.VARIABLE -> ASTBuilders.Variable + else -> throw ParserException.unsupportedLiteralType(tokenType) + } + } + } + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param isSingleArg ๋‹จ์ผ ์ธ์ˆ˜์ธ์ง€ ์—ฌ๋ถ€ + * @return ์ธ์ˆ˜ ๋ชฉ๋ก AST ๋นŒ๋” + */ + fun createArgumentListBuilder(isSingleArg: Boolean): ASTBuilderContract { + val cacheKey = "args:$isSingleArg" + + return builderCache.getOrPut(cacheKey) { + if (isSingleArg) { + ASTBuilders.ArgsSingle + } else { + ASTBuilders.ArgsMultiple + } + } + } + + /** + * ๊ด„ํ˜ธ ํ‘œํ˜„์‹ AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param preserveParentheses ๊ด„ํ˜ธ ์ •๋ณด ๋ณด์กด ์—ฌ๋ถ€ + * @return ๊ด„ํ˜ธ ํ‘œํ˜„์‹ AST ๋นŒ๋” + */ + fun createParenthesizedBuilder(preserveParentheses: Boolean = false): ASTBuilderContract { + val cacheKey = "paren:$preserveParentheses" + + return builderCache.getOrPut(cacheKey) { + ASTBuilders.Parenthesized + } + } + + /** + * ์‹๋ณ„์ž(Identity) AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹๋ณ„์ž AST ๋นŒ๋” + */ + fun createIdentityBuilder(): ASTBuilderContract { + return ASTBuilders.Identity + } + + /** + * ์—ก์‹ค๋ก  AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ก์‹ค๋ก  AST ๋นŒ๋” + */ + fun createEpsilonBuilder(): ASTBuilderContract { + return ASTBuilders.Identity + } + + /** + * ์‹œ์ž‘ ์‹ฌ๋ณผ AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹œ์ž‘ ์‹ฌ๋ณผ AST ๋นŒ๋” + */ + fun createStartBuilder(): ASTBuilderContract { + return ASTBuilders.Start + } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ AST ๋นŒ๋”๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ๋นŒ๋” ์ด๋ฆ„ + * @param buildFunction ๋นŒ๋“œ ํ•จ์ˆ˜ + * @return ์‚ฌ์šฉ์ž ์ •์˜ AST ๋นŒ๋” + */ + fun createCustomBuilder( + name: String, + buildFunction: (List) -> Any? + ): ASTBuilderContract { + if (name.isBlank()) { + throw ParserException.builderNameBlank(name) + } + + val cacheKey = "custom:$name" + + return builderCache.getOrPut(cacheKey) { + object : ASTBuilderContract { + override fun build(children: List): Any { + return buildFunction(children) ?: "" + } + + override fun toString(): String = "CustomBuilder($name)" + } + } + } + + /** + * ํ† ํฐ ํƒ€์ž…์œผ๋กœ๋ถ€ํ„ฐ ์ ์ ˆํ•œ AST ๋นŒ๋”๋ฅผ ์ž๋™ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ํ† ํฐ ํƒ€์ž… + * @param context ์ถ”๊ฐ€ ์ปจํ…์ŠคํŠธ ์ •๋ณด + * @return ์„ ํƒ๋œ AST ๋นŒ๋” + */ + fun selectBuilderForToken( + tokenType: TokenType, + context: Map = emptyMap() + ): ASTBuilderContract { + return when { + tokenType.isArithmeticOperator() -> createArithmeticBuilder(tokenType) + tokenType.isLogicalOperator() -> createLogicalBuilder(tokenType) + tokenType.isComparisonOperator() -> createComparisonBuilder(tokenType) + tokenType.isLiteral -> createLiteralBuilder(tokenType) + tokenType == TokenType.LEFT_PAREN -> createParenthesizedBuilder() + tokenType == TokenType.IF -> createConditionalBuilder() + else -> createIdentityBuilder() + } + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ํŒจํ„ด์œผ๋กœ๋ถ€ํ„ฐ ์ ์ ˆํ•œ AST ๋นŒ๋”๋ฅผ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. + * + * @param leftSymbol ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param rightSymbols ์šฐ๋ณ€ ์‹ฌ๋ณผ๋“ค + * @return ์„ ํƒ๋œ AST ๋นŒ๋” + */ + fun selectBuilderForProduction( + leftSymbol: TokenType, + rightSymbols: List + ): ASTBuilderContract { + return when (rightSymbols.size) { + 0 -> createEpsilonBuilder() + 1 -> { + val symbol = rightSymbols[0] + if (symbol.isLiteral) { + createLiteralBuilder(symbol) + } else { + createIdentityBuilder() + } + } + 2 -> { + // ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ํŒจํ„ด ๊ฒ€์‚ฌ + val (first, second) = rightSymbols + if (first.isOperator) { + createUnaryOperatorBuilder(getOperatorString(first)) + } else { + createIdentityBuilder() + } + } + 3 -> { + // ์ดํ•ญ ์—ฐ์‚ฐ์ž ๋˜๋Š” ๊ด„ํ˜ธ ํŒจํ„ด ๊ฒ€์‚ฌ + val (first, second, third) = rightSymbols + when { + first == TokenType.LEFT_PAREN && third == TokenType.RIGHT_PAREN -> + createParenthesizedBuilder() + second.isOperator -> + createBinaryOperatorBuilder(getOperatorString(second)) + else -> + createIdentityBuilder() + } + } + 4 -> { + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ํŒจํ„ด ๊ฒ€์‚ฌ (identifier ( args )) + if (rightSymbols[0] == TokenType.IDENTIFIER && + rightSymbols[1] == TokenType.LEFT_PAREN && + rightSymbols[3] == TokenType.RIGHT_PAREN) { + createFunctionCallBuilder(hasArguments = true) + } else { + createIdentityBuilder() + } + } + 8 -> { + // ์กฐ๊ฑด๋ถ€ ํ‘œํ˜„์‹ ํŒจํ„ด ๊ฒ€์‚ฌ (if ( expr , expr , expr )) + if (rightSymbols[0] == TokenType.IF && + rightSymbols[1] == TokenType.LEFT_PAREN && + rightSymbols[7] == TokenType.RIGHT_PAREN) { + createConditionalBuilder() + } else { + createIdentityBuilder() + } + } + else -> createIdentityBuilder() + } + } + + /** + * ๋นŒ๋” ์บ์‹œ๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param force ๊ฐ•์ œ ์ •๋ฆฌ ์—ฌ๋ถ€ + */ + fun clearCache(force: Boolean = false) { + if (force || builderCache.size > MAX_CACHE_SIZE) { + builderCache.clear() + } + } + + /** + * ์บ์‹œ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์บ์‹œ ํ†ต๊ณ„ ๋งต + */ + fun getCacheStatistics(): Map = mapOf( + "cacheSize" to builderCache.size, + "maxCacheSize" to MAX_CACHE_SIZE, + "cacheUtilization" to (builderCache.size.toDouble() / MAX_CACHE_SIZE), + "cachedBuilderTypes" to builderCache.keys.map { it.split(":")[0] }.distinct() + ) + + /** + * ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + */ + private fun validateOperator(operator: String) { + if (operator.isBlank()) { + throw ParserException.operatorBlank(operator) + } + if (operator.length > 3) { + throw ParserException.operatorTooLong(operator, maxLength = 3) + } + } + + /** + * ํ† ํฐ ํƒ€์ž…์œผ๋กœ๋ถ€ํ„ฐ ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ํ† ํฐ ํƒ€์ž… + * @return ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + */ + private fun getOperatorString(tokenType: TokenType): String { + return when (tokenType) { + TokenType.PLUS -> "+" + TokenType.MINUS -> "-" + TokenType.MULTIPLY -> "*" + TokenType.DIVIDE -> "/" + TokenType.MODULO -> "%" + TokenType.POWER -> "^" + TokenType.AND -> "&&" + TokenType.OR -> "||" + TokenType.NOT -> "!" + TokenType.EQUAL -> "==" + TokenType.NOT_EQUAL -> "!=" + TokenType.LESS -> "<" + TokenType.LESS_EQUAL -> "<=" + TokenType.GREATER -> ">" + TokenType.GREATER_EQUAL -> ">=" + else -> tokenType.name + } + } + + /** + * ํŒฉํ† ๋ฆฌ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxCacheSize" to MAX_CACHE_SIZE, + "supportedBuilderTypes" to listOf( + "binary", "unary", "arithmetic", "logical", "comparison", + "functionCall", "conditional", "literal", "arguments", + "parenthesized", "identity", "epsilon", "start", "custom" + ), + "cacheEnabled" to true, + "autoSelection" to true + ) + + /** + * ํŒฉํ† ๋ฆฌ ์‚ฌ์šฉ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "factoryName" to "ASTBuilderContractFactory", + "builderCreationMethods" to 13, + "autoSelectionMethods" to 2, + "cacheStatistics" to getCacheStatistics() + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/LRItemFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/LRItemFactory.kt new file mode 100644 index 00000000..de2a3fa9 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/LRItemFactory.kt @@ -0,0 +1,384 @@ +package hs.kr.entrydsm.domain.parser.factories + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity + +/** + * LR ์•„์ดํ…œ ์ƒ์„ฑ์„ ๋‹ด๋‹นํ•˜๋Š” ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Factory ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ LR ์•„์ดํ…œ์˜ ๋ณต์žกํ•œ ์ƒ์„ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ , + * ๋‹ค์–‘ํ•œ ์œ ํ˜•์˜ LR ์•„์ดํ…œ ์ƒ์„ฑ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ตฌ์ถ• ๊ณผ์ •์—์„œ + * ํ•„์š”ํ•œ ๋‹ค์–‘ํ•œ LR ์•„์ดํ…œ๋“ค์„ ์ผ๊ด€๋œ ๋ฐฉ์‹์œผ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Factory( + context = "parser", + complexity = Complexity.NORMAL, + cache = true +) +class LRItemFactory { + + companion object { + private const val MAX_PRODUCTION_LENGTH = 50 + private const val MAX_LOOKAHEAD_SIZE = 100 + } + + /** + * ๊ธฐ๋ณธ LR ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param production ์ƒ์‚ฐ ๊ทœ์น™ + * @param dotPos ์ ์˜ ์œ„์น˜ + * @param lookahead ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ์ƒ์„ฑ๋œ LR ์•„์ดํ…œ + */ + fun createLRItem( + production: Production, + dotPos: Int = 0, + lookahead: Set = emptySet() + ): LRItem { + validateProduction(production) + validateDotPosition(production, dotPos) + validateLookahead(lookahead) + + return LRItem( + production = production, + dotPos = dotPos, + lookahead = lookahead.first() + ) + } + + /** + * ์ปค๋„ LR ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์ปค๋„ ์•„์ดํ…œ์€ ์ƒํƒœ๋ฅผ ๊ณ ์œ ํ•˜๊ฒŒ ์‹๋ณ„ํ•˜๋Š” ์•„์ดํ…œ๋“ค์ž…๋‹ˆ๋‹ค. + * + * @param production ์ƒ์‚ฐ ๊ทœ์น™ + * @param dotPos ์ ์˜ ์œ„์น˜ (0๋ณด๋‹ค ์ปค์•ผ ํ•จ) + * @param lookahead ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ์ปค๋„ LR ์•„์ดํ…œ + */ + fun createKernelItem( + production: Production, + dotPos: Int, + lookahead: Set + ): LRItem { + if (!(dotPos > 0 || production.id == -1)) { + throw ParserException.kernelDotPositionInvalid(dotPos, production.id) + } + + return createLRItem(production, dotPos, lookahead) + } + + /** + * ๋น„์ปค๋„ LR ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ๋น„์ปค๋„ ์•„์ดํ…œ์€ ํด๋กœ์ € ์—ฐ์‚ฐ์œผ๋กœ ์ถ”๊ฐ€๋˜๋Š” ์•„์ดํ…œ๋“ค์ž…๋‹ˆ๋‹ค. + * + * @param production ์ƒ์‚ฐ ๊ทœ์น™ + * @param lookahead ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ๋น„์ปค๋„ LR ์•„์ดํ…œ + */ + fun createNonKernelItem( + production: Production, + lookahead: Set + ): LRItem { + return createLRItem(production, 0, lookahead) + } + + /** + * ์‹œ์ž‘ LR ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startProduction ์‹œ์ž‘ ์ƒ์‚ฐ ๊ทœ์น™ + * @param endOfInputSymbol ์ž…๋ ฅ ๋ ์‹ฌ๋ณผ + * @return ์‹œ์ž‘ LR ์•„์ดํ…œ + */ + fun createStartItem( + startProduction: Production, + endOfInputSymbol: TokenType = TokenType.DOLLAR + ): LRItem { + if (startProduction.id != -1) { + throw ParserException.startItemMustUseAugmented(startProduction.id) + } + + return createLRItem( + production = startProduction, + dotPos = 0, + lookahead = setOf(endOfInputSymbol) + ) + } + + /** + * ์™„์„ฑ๋œ LR ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์ ์ด ์ƒ์‚ฐ ๊ทœ์น™์˜ ๋์— ์œ„์น˜ํ•œ ์•„์ดํ…œ์ž…๋‹ˆ๋‹ค. + * + * @param production ์ƒ์‚ฐ ๊ทœ์น™ + * @param lookahead ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ์™„์„ฑ๋œ LR ์•„์ดํ…œ + */ + fun createCompleteItem( + production: Production, + lookahead: Set + ): LRItem { + val dotPos = production.right.size + return createLRItem(production, dotPos, lookahead) + } + + /** + * ์ ์„ ํ•œ ์œ„์น˜ ์ด๋™ํ•œ LR ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param item ์›๋ณธ LR ์•„์ดํ…œ + * @return ์ ์ด ์ด๋™๋œ ์ƒˆ LR ์•„์ดํ…œ + * @throws IllegalStateException ์ ์„ ๋” ์ด์ƒ ์ด๋™ํ•  ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun createAdvancedItem(item: LRItem): LRItem { + if (item.isComplete()) { + throw ParserException.itemAlreadyComplete(item) + } + + return createLRItem( + production = item.production, + dotPos = item.dotPos + 1, + lookahead = setOf(item.lookahead) + ) + } + + /** + * ์ƒˆ๋กœ์šด ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ์„ ์ถ”๊ฐ€ํ•œ LR ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param item ์›๋ณธ LR ์•„์ดํ…œ + * @param newLookaheads ์ถ”๊ฐ€ํ•  ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ๋“ค + * @return ์ „๋ฐฉํƒ์ƒ‰์ด ์ถ”๊ฐ€๋œ ์ƒˆ LR ์•„์ดํ…œ + */ + fun createItemWithLookaheads( + item: LRItem, + newLookaheads: Set + ): LRItem { + val combinedLookaheads = setOf(item.lookahead) + newLookaheads + validateLookahead(combinedLookaheads) + + return createLRItem( + production = item.production, + dotPos = item.dotPos, + lookahead = combinedLookaheads + ) + } + + /** + * ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ์„ ๋Œ€์ฒดํ•œ LR ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param item ์›๋ณธ LR ์•„์ดํ…œ + * @param newLookaheads ์ƒˆ๋กœ์šด ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ๋“ค + * @return ์ „๋ฐฉํƒ์ƒ‰์ด ๋Œ€์ฒด๋œ ์ƒˆ LR ์•„์ดํ…œ + */ + fun createItemWithReplacedLookaheads( + item: LRItem, + newLookaheads: Set + ): LRItem { + validateLookahead(newLookaheads) + + return createLRItem( + production = item.production, + dotPos = item.dotPos, + lookahead = newLookaheads + ) + } + + /** + * LR(0) ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (์ „๋ฐฉํƒ์ƒ‰ ์—†์Œ). + * + * @param production ์ƒ์‚ฐ ๊ทœ์น™ + * @param dotPos ์ ์˜ ์œ„์น˜ + * @return LR(0) ์•„์ดํ…œ + */ + fun createLR0Item( + production: Production, + dotPos: Int = 0 + ): LRItem { + return createLRItem(production, dotPos, emptySet()) + } + + /** + * LR(1) ์•„์ดํ…œ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (๋‹จ์ผ ์ „๋ฐฉํƒ์ƒ‰). + * + * @param production ์ƒ์‚ฐ ๊ทœ์น™ + * @param dotPos ์ ์˜ ์œ„์น˜ + * @param lookahead ๋‹จ์ผ ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ + * @return LR(1) ์•„์ดํ…œ + */ + fun createLR1Item( + production: Production, + dotPos: Int = 0, + lookahead: TokenType + ): LRItem { + return createLRItem(production, dotPos, setOf(lookahead)) + } + + /** + * ๋‹ค์ค‘ LR ์•„์ดํ…œ๋“ค์„ ํ•œ ๋ฒˆ์— ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param production ์ƒ์‚ฐ ๊ทœ์น™ + * @param lookaheads ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ๋“ค (๊ฐ๊ฐ ๋ณ„๋„ ์•„์ดํ…œ ์ƒ์„ฑ) + * @param dotPos ์ ์˜ ์œ„์น˜ + * @return ์ƒ์„ฑ๋œ LR ์•„์ดํ…œ๋“ค์˜ ์ง‘ํ•ฉ + */ + fun createMultipleItems( + production: Production, + lookaheads: Set, + dotPos: Int = 0 + ): Set { + validateProduction(production) + validateDotPosition(production, dotPos) + validateLookahead(lookaheads) + + return lookaheads.map { lookahead -> + createLRItem(production, dotPos, setOf(lookahead)) + }.toSet() + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์œผ๋กœ๋ถ€ํ„ฐ ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ LR ์•„์ดํ…œ๋“ค์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param production ์ƒ์‚ฐ ๊ทœ์น™ + * @param lookaheads ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ๋“ค + * @return ๋ชจ๋“  ์  ์œ„์น˜์— ๋Œ€ํ•œ LR ์•„์ดํ…œ๋“ค + */ + fun createAllItemsForProduction( + production: Production, + lookaheads: Set = emptySet() + ): List { + validateProduction(production) + validateLookahead(lookaheads) + + val items = mutableListOf() + + // ์ ์˜ ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ์œ„์น˜์— ๋Œ€ํ•ด ์•„์ดํ…œ ์ƒ์„ฑ + for (dotPos in 0..production.right.size) { + items.add(createLRItem(production, dotPos, lookaheads)) + } + + return items + } + + /** + * ๊ธฐ์กด ์•„์ดํ…œ๋“ค์„ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * ๋™์ผํ•œ production๊ณผ dotPos์„ ๊ฐ€์ง„ ์•„์ดํ…œ๋“ค์˜ lookahead๋ฅผ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param items ๋ณ‘ํ•ฉํ•  LR ์•„์ดํ…œ๋“ค + * @return ๋ณ‘ํ•ฉ๋œ LR ์•„์ดํ…œ๋“ค + */ + fun mergeItems(items: Collection): Set { + val grouped = items.groupBy { it.production to it.dotPos } + + return grouped.map { (key, itemList) -> + val (production, dotPos) = key + val mergedLookaheads = itemList.map { it.lookahead }.toSet() + createLRItem(production, dotPos, mergedLookaheads) + }.toSet() + } + + /** + * ์•„์ดํ…œ๋“ค์—์„œ ์ปค๋„ ์•„์ดํ…œ๋“ค๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param items ์•„์ดํ…œ๋“ค + * @return ์ปค๋„ ์•„์ดํ…œ๋“ค + */ + fun extractKernelItems(items: Set): Set { + return items.filter { it.isKernelItem() }.toSet() + } + + /** + * ์•„์ดํ…œ๋“ค์—์„œ ๋น„์ปค๋„ ์•„์ดํ…œ๋“ค๋งŒ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param items ์•„์ดํ…œ๋“ค + * @return ๋น„์ปค๋„ ์•„์ดํ…œ๋“ค + */ + fun extractNonKernelItems(items: Set): Set { + return items.filter { !it.isKernelItem() }.toSet() + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param production ๊ฒ€์ฆํ•  ์ƒ์‚ฐ ๊ทœ์น™ + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + private fun validateProduction(production: Production) { + if (production.right.size > MAX_PRODUCTION_LENGTH) { + throw ParserException.productionLengthExceedsLimit( + length = production.right.size, + maxLength = MAX_PRODUCTION_LENGTH + ) + } + } + + /** + * ์  ์œ„์น˜์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param production ์ƒ์‚ฐ ๊ทœ์น™ + * @param dotPos ์ ์˜ ์œ„์น˜ + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + private fun validateDotPosition(production: Production, dotPos: Int) { + if (dotPos < 0) { + throw ParserException.invalidDotPositionNegative(dotPos) + } + if (dotPos > production.right.size) { + throw ParserException.invalidDotPositionExceeds(dotPos, production.right.size) + } + } + + /** + * ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ๋“ค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param lookahead ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ๋“ค + * @throws IllegalArgumentException ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + private fun validateLookahead(lookahead: Set) { + if (lookahead.size > MAX_LOOKAHEAD_SIZE) { + throw ParserException.lookaheadSizeExceedsLimit( + size = lookahead.size, + maxSize = MAX_LOOKAHEAD_SIZE + ) + } + + lookahead.forEach { symbol -> + if (!symbol.isTerminal) { + throw ParserException.lookaheadNotTerminal(symbol) + } + } + } + + /** + * ํŒฉํ† ๋ฆฌ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxProductionLength" to MAX_PRODUCTION_LENGTH, + "maxLookaheadSize" to MAX_LOOKAHEAD_SIZE, + "supportedOperations" to listOf( + "createLRItem", "createKernelItem", "createNonKernelItem", + "createStartItem", "createCompleteItem", "createAdvancedItem", + "createLR0Item", "createLR1Item", "createMultipleItems", + "mergeItems", "extractKernelItems", "extractNonKernelItems" + ) + ) + + /** + * ํŒฉํ† ๋ฆฌ ์‚ฌ์šฉ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "factoryName" to "LRItemFactory", + "creationMethods" to 12, + "validationRules" to 3, + "utilityMethods" to 3 + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ParserFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ParserFactory.kt new file mode 100644 index 00000000..a79d75bf --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ParserFactory.kt @@ -0,0 +1,339 @@ +package hs.kr.entrydsm.domain.parser.factories + +import hs.kr.entrydsm.domain.ast.factory.ASTBuilders +import hs.kr.entrydsm.domain.parser.aggregates.LRParser +import hs.kr.entrydsm.domain.parser.aggregates.LRParserTable +import hs.kr.entrydsm.domain.parser.entities.* +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity + +/** + * Parser ๋„๋ฉ”์ธ์˜ ๊ฐ์ฒด๋“ค์„ ์ƒ์„ฑํ•˜๋Š” ํŒฉํ† ๋ฆฌ์ž…๋‹ˆ๋‹ค. + * + * ๋‹ค์–‘ํ•œ ํƒ€์ž…์˜ ํŒŒ์„œ์™€ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•˜๋ฉฐ, ๋„๋ฉ”์ธ ๊ทœ์น™๊ณผ ์ •์ฑ…์„ + * ์ ์šฉํ•˜์—ฌ ์ผ๊ด€๋œ ๊ฐ์ฒด ์ƒ์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Factory(context = "parser", complexity = Complexity.HIGH, cache = true) +class ParserFactory { + + // private val parserSpecification = ParserSpecification() + // private val parserPolicy = ParserPolicy() + + /** + * ์ž…๋ ฅ ๋ฌธ์ž์—ด๋กœ๋ถ€ํ„ฐ ์ ์ ˆํ•œ ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ํŒŒ์‹ฑํ•  ์ž…๋ ฅ ๋ฌธ์ž์—ด + * @param options ํŒŒ์„œ ์˜ต์…˜ + * @return ์ƒ์„ฑ๋œ ํŒŒ์„œ ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ์ž…๋ ฅ์ด ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun createParser( + input: String, + options: ParserOptions = ParserOptions.default() + ): LRParser { + // ์ž…๋ ฅ ๊ฒ€์ฆ + if (input.isBlank()) { + throw ParserException.inputBlank() + } + + // ์ •์ฑ… ์ ์šฉ + // parserPolicy.validateInput(input, options) + + return when (options.parserType) { + ParserType.LR1 -> createLR1Parser(input, options) + ParserType.LALR -> createLALRParser(input, options) + ParserType.SLR -> createSLRParser(input, options) + } + } + + /** + * LR(1) ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private fun createLR1Parser(input: String, options: ParserOptions): LRParser { + val parserTable = createParserTable(ParserTableType.LR1, options) + val astBuilders = createASTBuilders(options.grammarType) + + return LRParser() + } + + /** + * LALR ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private fun createLALRParser(input: String, options: ParserOptions): LRParser { + val parserTable = createParserTable(ParserTableType.LALR, options) + val astBuilders = createASTBuilders(options.grammarType) + + return LRParser() + } + + /** + * SLR ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private fun createSLRParser(input: String, options: ParserOptions): LRParser { + val parserTable = createParserTable(ParserTableType.SLR, options) + val astBuilders = createASTBuilders(options.grammarType) + + return LRParser() + } + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createParserTable( + tableType: ParserTableType, + options: ParserOptions = ParserOptions.default() + ): LRParserTable { + // ํ…Œ์ด๋ธ” ์ƒ์„ฑ ์ •์ฑ… ์ ์šฉ (์ฃผ์„ ์ฒ˜๋ฆฌ๋จ) + // parserPolicy.validateTableType(tableType, options) + + return when (tableType) { + ParserTableType.LR1 -> LRParserTable.createWithDefaultGrammar() + ParserTableType.LALR -> LRParserTable.createWithDefaultGrammar() + ParserTableType.SLR -> LRParserTable.createWithDefaultGrammar() + } + } + + /** + * AST ๋นŒ๋” ๋งต์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createASTBuilders(grammarType: GrammarType): Map { + return when (grammarType) { + GrammarType.CALCULATOR -> mapOf(0 to ASTBuilders.Identity) + GrammarType.EXPRESSION -> mapOf(0 to ASTBuilders.Identity) + GrammarType.EXTENDED -> mapOf(0 to ASTBuilders.Identity) + GrammarType.CUSTOM -> mapOf(0 to ASTBuilders.Identity) + } + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createProduction( + id: Int, + left: String, + right: List, + description: String + ): Production { + // ์ƒ์‚ฐ ๊ทœ์น™ ์ƒ์„ฑ ์ •์ฑ… ์ ์šฉ (์ฃผ์„ ์ฒ˜๋ฆฌ๋จ) + // parserPolicy.validateProduction(id, left, right, description) + + // val leftToken = parserPolicy.parseTokenType(left) + // val rightTokens = right.map { parserPolicy.parseTokenType(it) } + + return Production(id, TokenType.EXPR, emptyList(), ASTBuilders.Identity) + } + + /** + * LR ์•ก์…˜์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createLRAction( + actionType: String, + parameter: Any? = null + ): LRAction { + return when (actionType.lowercase()) { + "shift", "s" -> { + val state = parameter as? Int ?: throw ParserException.shiftStateRequired() + LRAction.Shift(state) + } + "reduce", "r" -> { + val production = parameter as? Production ?: throw ParserException.reduceProductionRequired() + LRAction.Reduce(production) + } + "accept", "acc" -> LRAction.Accept + "error", "err" -> { + val message = parameter as? String ?: "๊ตฌ๋ฌธ ์˜ค๋ฅ˜" + LRAction.Error(errorCode = null, errorMessage = message) + } + else -> throw ParserException.unsupportedActionType(actionType) + } + } + + /** + * ์••์ถ•๋œ LR ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createCompressedState( + id: Int, + kernelItems: Set, + productions: List + ): CompressedLRState { + // ์ƒํƒœ ์ƒ์„ฑ ์ •์ฑ… ์ ์šฉ (์ฃผ์„ ์ฒ˜๋ฆฌ๋จ) + // parserPolicy.validateStateCreation(id, kernelItems) + + return CompressedLRState.fromItems(kernelItems, true) + } + + /** + * ์ตœ์ ํ™”๋œ ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createOptimizedParser( + input: String, + optimizations: List = listOf(OptimizationType.LALR, OptimizationType.TABLE_COMPRESSION) + ): LRParser { + val options = ParserOptions( + parserType = ParserType.LALR, + enableOptimizations = true, + optimizations = optimizations + ) + + return createParser(input, options) + } + + /** + * ๋””๋ฒ„๊ทธ ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun createDebugParser( + input: String, + enableTracing: Boolean = true, + enableStatistics: Boolean = true + ): LRParser { + val options = ParserOptions( + parserType = ParserType.LR1, + enableDebugging = true, + enableTracing = enableTracing, + enableStatistics = enableStatistics + ) + + return createParser(input, options) + } + + /** + * ํŒŒ์„œ ํŒฉํ† ๋ฆฌ์˜ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getFactoryStatistics(): Map { + return mapOf( + "totalParsersCreated" to createdParserCount, + "totalTablesCreated" to createdTableCount, + "supportedParserTypes" to ParserType.values().map { it.name }, + "supportedTableTypes" to ParserTableType.values().map { it.name }, + "supportedGrammarTypes" to GrammarType.values().map { it.name }, + "factoryComplexity" to Complexity.HIGH.name, + "cacheEnabled" to true + ) + } + + companion object { + private var createdParserCount = 0L + private var createdTableCount = 0L + + /** + * ์‹ฑ๊ธ€ํ†ค ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + @JvmStatic + fun getInstance(): ParserFactory = ParserFactory() + + /** + * ๊ธฐ๋ณธ ์„ค์ •์œผ๋กœ ํŒŒ์„œ๋ฅผ ์ƒ์„ฑํ•˜๋Š” ํŽธ์˜ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + */ + @JvmStatic + fun createDefaultParser(input: String): LRParser { + return getInstance().createParser(input) + } + + /** + * ๋น ๋ฅธ ํŒŒ์‹ฑ์„ ์œ„ํ•œ ํŽธ์˜ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + */ + @JvmStatic + fun quickParse(input: String): Any { + // ๊ฐ„๋‹จํ•œ ํ† ํฐํ™”๋ฅผ ์ˆ˜ํ–‰ํ•˜์—ฌ List์œผ๋กœ ๋ณ€ํ™˜ + val tokens = mutableListOf() + // ์ž„์‹œ ๊ตฌํ˜„: ์‹ค์ œ๋กœ๋Š” Lexer๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•จ + tokens.add(Token.eof()) + return createDefaultParser(input).parse(tokens) + } + } + + init { + createdParserCount++ + } +} + +/** + * ํŒŒ์„œ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ +enum class ParserType { + /** LR(1) ํŒŒ์„œ */ + LR1, + /** LALR ํŒŒ์„œ */ + LALR, + /** SLR ํŒŒ์„œ */ + SLR +} + +/** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ +enum class ParserTableType { + /** LR(1) ํ…Œ์ด๋ธ” */ + LR1, + /** LALR ํ…Œ์ด๋ธ” */ + LALR, + /** SLR ํ…Œ์ด๋ธ” */ + SLR +} + +/** + * ๋ฌธ๋ฒ• ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ +enum class GrammarType { + /** ๊ณ„์‚ฐ๊ธฐ ๋ฌธ๋ฒ• */ + CALCULATOR, + /** ํ‘œํ˜„์‹ ๋ฌธ๋ฒ• */ + EXPRESSION, + /** ํ™•์žฅ๋œ ๋ฌธ๋ฒ• */ + EXTENDED, + /** ์‚ฌ์šฉ์ž ์ •์˜ ๋ฌธ๋ฒ• */ + CUSTOM +} + +/** + * ์ตœ์ ํ™” ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ +enum class OptimizationType { + /** LALR ์ตœ์ ํ™” */ + LALR, + /** ํ…Œ์ด๋ธ” ์••์ถ• */ + TABLE_COMPRESSION, + /** ์ƒํƒœ ์บ์‹ฑ */ + STATE_CACHING, + /** ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™” */ + MEMORY_OPTIMIZATION +} + +/** + * ํŒŒ์„œ ์˜ต์…˜์„ ์ •์˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class ParserOptions( + val parserType: ParserType = ParserType.LALR, + val grammarType: GrammarType = GrammarType.CALCULATOR, + val enableOptimizations: Boolean = true, + val optimizations: List = listOf(OptimizationType.LALR), + val enableDebugging: Boolean = false, + val enableTracing: Boolean = false, + val enableStatistics: Boolean = false, + val maxInputLength: Int = 100_000, + val maxParsingSteps: Int = 1_000_000 +) { + companion object { + fun default(): ParserOptions = ParserOptions() + + fun optimized(): ParserOptions = ParserOptions( + enableOptimizations = true, + optimizations = OptimizationType.values().toList() + ) + + fun debug(): ParserOptions = ParserOptions( + enableDebugging = true, + enableTracing = true, + enableStatistics = true + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ParsingStateFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ParsingStateFactory.kt new file mode 100644 index 00000000..8d161252 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ParsingStateFactory.kt @@ -0,0 +1,530 @@ +package hs.kr.entrydsm.domain.parser.factories + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.domain.parser.entities.ParsingState +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity + +/** + * Parsing State ์ƒ์„ฑ์„ ๋‹ด๋‹นํ•˜๋Š” ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Factory ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํŒŒ์‹ฑ ์ƒํƒœ์˜ ๋ณต์žกํ•œ ์ƒ์„ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ , + * LR ํŒŒ์„œ ํ…Œ์ด๋ธ” ๊ตฌ์ถ• ๊ณผ์ •์—์„œ ํ•„์š”ํ•œ ๋‹ค์–‘ํ•œ ํŒŒ์‹ฑ ์ƒํƒœ๋“ค์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์ƒํƒœ ์ƒ์„ฑ, ์ „์ด ๊ด€๋ฆฌ, ์•ก์…˜/goto ํ…Œ์ด๋ธ” ์„ค์ •์„ ํ†ตํ•ฉ์ ์œผ๋กœ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Factory( + context = "parser", + complexity = Complexity.NORMAL, + cache = true +) +class ParsingStateFactory { + + companion object { + private const val MAX_STATE_COUNT = 10000 + private const val MAX_ITEMS_PER_STATE = 1000 + private const val MAX_TRANSITIONS_PER_STATE = 500 + private var nextStateId = 0 + } + + /** + * ๊ธฐ๋ณธ ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒํƒœ ID + * @param items LR ์•„์ดํ…œ๋“ค + * @param isAccepting ์ˆ˜๋ฝ ์ƒํƒœ ์—ฌ๋ถ€ + * @param isFinal ์ตœ์ข… ์ƒํƒœ ์—ฌ๋ถ€ + * @return ์ƒ์„ฑ๋œ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createParsingState( + id: Int, + items: Set, + isAccepting: Boolean = false, + isFinal: Boolean = false + ): ParsingState { + validateStateData(id, items) + + return ParsingState( + id = id, + items = items, + isAccepting = isAccepting, + isFinal = isFinal + ) + } + + /** + * ์ž๋™ ID ํ• ๋‹น์œผ๋กœ ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param items LR ์•„์ดํ…œ๋“ค + * @param isAccepting ์ˆ˜๋ฝ ์ƒํƒœ ์—ฌ๋ถ€ + * @param isFinal ์ตœ์ข… ์ƒํƒœ ์—ฌ๋ถ€ + * @return ์ƒ์„ฑ๋œ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createParsingState( + items: Set, + isAccepting: Boolean = false, + isFinal: Boolean = false + ): ParsingState { + val id = generateNextId() + return createParsingState(id, items, isAccepting, isFinal) + } + + /** + * ์ดˆ๊ธฐ ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param startProduction ์‹œ์ž‘ ์ƒ์‚ฐ ๊ทœ์น™ + * @param endOfInputSymbol ์ž…๋ ฅ ๋ ์‹ฌ๋ณผ + * @return ์ดˆ๊ธฐ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createInitialState( + startProduction: Production, + endOfInputSymbol: TokenType = TokenType.DOLLAR + ): ParsingState { + val startItem = LRItem( + production = startProduction, + dotPos = 0, + lookahead = endOfInputSymbol + ) + + return createParsingState( + id = 0, + items = setOf(startItem), + isAccepting = false, + isFinal = false + ) + } + + /** + * ์ˆ˜๋ฝ ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒํƒœ ID (์˜ต์…˜) + * @param completeItems ์™„์„ฑ๋œ ์•„์ดํ…œ๋“ค + * @return ์ˆ˜๋ฝ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createAcceptingState( + id: Int? = null, + completeItems: Set + ): ParsingState { + val stateId = id ?: generateNextId() + + if (!completeItems.all { it.isComplete() }) { + throw ParserException.acceptingStateItemsNotComplete() + } + + return createParsingState( + id = stateId, + items = completeItems, + isAccepting = true, + isFinal = true + ) + } + + /** + * ์—๋Ÿฌ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒํƒœ ID (์˜ต์…˜) + * @param errorContext ์—๋Ÿฌ ์ปจํ…์ŠคํŠธ ์ •๋ณด + * @return ์—๋Ÿฌ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createErrorState( + id: Int? = null, + errorContext: Map = emptyMap() + ): ParsingState { + val stateId = id ?: generateNextId() + val errorItem = LRItem( + production = Production(-1, TokenType.START, listOf(TokenType.EPSILON)), + dotPos = 0, + lookahead = TokenType.DOLLAR + ) + + return ParsingState( + id = stateId, + items = setOf(errorItem), + isFinal = true, + metadata = errorContext + ("isError" to true) + ) + } + + /** + * ํด๋กœ์ € ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param kernelItems ์ปค๋„ ์•„์ดํ…œ๋“ค + * @param productions ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™๋“ค + * @param firstSets FIRST ์ง‘ํ•ฉ๋“ค + * @return ํด๋กœ์ €๊ฐ€ ์ ์šฉ๋œ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createStateWithClosure( + kernelItems: Set, + productions: List, + firstSets: Map> = emptyMap() + ): ParsingState { + val allItems = mutableSetOf() + allItems.addAll(kernelItems) + + // ํด๋กœ์ € ์—ฐ์‚ฐ ์ˆ˜ํ–‰ + val workList = ArrayDeque(kernelItems) + + while (workList.isNotEmpty()) { + val item = workList.removeFirst() + val nextSymbol = item.nextSymbol() + + if (nextSymbol?.isNonTerminal() == true) { + // ํ•ด๋‹น ๋…ผํ„ฐ๋ฏธ๋„์— ๋Œ€ํ•œ ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™ ์ถ”๊ฐ€ + val relevantProductions = productions.filter { it.left == nextSymbol } + + for (production in relevantProductions) { + val beta = item.beta() + val lookaheads = calculateLookaheads(beta, setOf(item.lookahead), firstSets) + + val newItem = LRItem( + production = production, + dotPos = 0, + lookahead = lookaheads.first() + ) + + if (newItem !in allItems) { + allItems.add(newItem) + workList.add(newItem) + } + } + } + } + + return createParsingState(items = allItems) + } + + /** + * GOTO ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param sourceState ์›๋ณธ ์ƒํƒœ + * @param symbol ์ „์ด ์‹ฌ๋ณผ + * @param productions ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™๋“ค + * @param firstSets FIRST ์ง‘ํ•ฉ๋“ค + * @return GOTO ์—ฐ์‚ฐ ๊ฒฐ๊ณผ ์ƒํƒœ (null์ด๋ฉด ์ „์ด ๋ถˆ๊ฐ€) + */ + fun createStateWithGoto( + sourceState: ParsingState, + symbol: TokenType, + productions: List, + firstSets: Map> = emptyMap() + ): ParsingState? { + val gotoItems = mutableSetOf() + + // symbol๋กœ ์ „์ด ๊ฐ€๋Šฅํ•œ ์•„์ดํ…œ๋“ค ์ˆ˜์ง‘ + for (item in sourceState.items) { + if (item.nextSymbol() == symbol) { + val advancedItem = LRItem( + production = item.production, + dotPos = item.dotPos + 1, + lookahead = item.lookahead + ) + gotoItems.add(advancedItem) + } + } + + if (gotoItems.isEmpty()) { + return null + } + + // ํด๋กœ์ € ์ ์šฉํ•˜์—ฌ ์™„์ „ํ•œ ์ƒํƒœ ์ƒ์„ฑ + return createStateWithClosure(gotoItems, productions, firstSets) + } + + /** + * ์•ก์…˜ ํ…Œ์ด๋ธ”์ด ํฌํ•จ๋œ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param baseState ๊ธฐ๋ณธ ์ƒํƒœ + * @param actions ์•ก์…˜ ํ…Œ์ด๋ธ” + * @return ์•ก์…˜์ด ์„ค์ •๋œ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createStateWithActions( + baseState: ParsingState, + actions: Map + ): ParsingState { + validateActions(actions) + + return baseState.copy(actions = actions) + } + + /** + * Goto ํ…Œ์ด๋ธ”์ด ํฌํ•จ๋œ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param baseState ๊ธฐ๋ณธ ์ƒํƒœ + * @param gotos Goto ํ…Œ์ด๋ธ” + * @return Goto๊ฐ€ ์„ค์ •๋œ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createStateWithGotos( + baseState: ParsingState, + gotos: Map + ): ParsingState { + validateGotos(gotos) + + return baseState.copy(gotos = gotos) + } + + /** + * ์™„์ „ํ•œ ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (์•ก์…˜ ๋ฐ Goto ํฌํ•จ). + * + * @param id ์ƒํƒœ ID + * @param items LR ์•„์ดํ…œ๋“ค + * @param actions ์•ก์…˜ ํ…Œ์ด๋ธ” + * @param gotos Goto ํ…Œ์ด๋ธ” + * @param transitions ์ „์ด ํ…Œ์ด๋ธ” + * @param isAccepting ์ˆ˜๋ฝ ์ƒํƒœ ์—ฌ๋ถ€ + * @param isFinal ์ตœ์ข… ์ƒํƒœ ์—ฌ๋ถ€ + * @return ์™„์ „ํ•œ ํŒŒ์‹ฑ ์ƒํƒœ + */ + fun createCompleteState( + id: Int, + items: Set, + actions: Map = emptyMap(), + gotos: Map = emptyMap(), + transitions: Map = emptyMap(), + isAccepting: Boolean = false, + isFinal: Boolean = false + ): ParsingState { + validateStateData(id, items) + validateActions(actions) + validateGotos(gotos) + validateTransitions(transitions) + + return ParsingState( + id = id, + items = items, + transitions = transitions, + actions = actions, + gotos = gotos, + isAccepting = isAccepting, + isFinal = isFinal + ) + } + + /** + * ์ƒํƒœ๋“ค์„ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * ๋™์ผํ•œ ์ปค๋„ ์•„์ดํ…œ์„ ๊ฐ€์ง„ ์ƒํƒœ๋“ค์˜ ์•„์ดํ…œ์„ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param states ๋ณ‘ํ•ฉํ•  ์ƒํƒœ๋“ค + * @return ๋ณ‘ํ•ฉ๋œ ์ƒํƒœ (null์ด๋ฉด ๋ณ‘ํ•ฉ ๋ถˆ๊ฐ€) + */ + fun mergeStates(states: Collection): ParsingState? { + if (states.isEmpty()) return null + if (states.size == 1) return states.first() + + val firstState = states.first() + val kernelItems = states.first().getKernelItems() + + // ๋ชจ๋“  ์ƒํƒœ๊ฐ€ ๋™์ผํ•œ ์ปค๋„ ์•„์ดํ…œ์„ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ + if (!states.all { it.getKernelItems() == kernelItems }) { + return null + } + + // ๋ชจ๋“  ์•„์ดํ…œ ๊ฒฐํ•ฉ + val mergedItems = states.flatMap { it.items }.toSet() + + return createParsingState( + id = firstState.id, + items = mergedItems, + isAccepting = states.any { it.isAccepting }, + isFinal = states.any { it.isFinal } + ) + } + + /** + * ์ƒํƒœ๋ฅผ ๋ณต์‚ฌํ•˜์—ฌ ์ƒˆ๋กœ์šด ID๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param original ์›๋ณธ ์ƒํƒœ + * @param newId ์ƒˆ๋กœ์šด ID (null์ด๋ฉด ์ž๋™ ์ƒ์„ฑ) + * @return ๋ณต์‚ฌ๋œ ์ƒํƒœ + */ + fun copyState(original: ParsingState, newId: Int? = null): ParsingState { + val id = newId ?: generateNextId() + + return original.copy(id = id) + } + + /** + * ์ƒํƒœ์˜ ์•„์ดํ…œ์„ ์ˆ˜์ •ํ•œ ์ƒˆ๋กœ์šด ์ƒํƒœ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param original ์›๋ณธ ์ƒํƒœ + * @param newItems ์ƒˆ๋กœ์šด ์•„์ดํ…œ๋“ค + * @param newId ์ƒˆ๋กœ์šด ID (null์ด๋ฉด ๊ธฐ์กด ID ์œ ์ง€) + * @return ์•„์ดํ…œ์ด ์ˆ˜์ •๋œ ์ƒˆ ์ƒํƒœ + */ + fun modifyStateItems( + original: ParsingState, + newItems: Set, + newId: Int? = null + ): ParsingState { + validateStateData(newId ?: original.id, newItems) + + return original.copy( + id = newId ?: original.id, + items = newItems + ) + } + + /** + * ๋‹ค์Œ ์ƒํƒœ ID๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹ค์Œ ID + */ + private fun generateNextId(): Int { + if (nextStateId >= MAX_STATE_COUNT) { + throw ParserException.stateCountExceedsLimit(nextStateId, MAX_STATE_COUNT) + } + return nextStateId++ + } + + /** + * Lookahead ์‹ฌ๋ณผ๋“ค์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param beta ๋ฒ ํƒ€ ๋ฌธ์ž์—ด + * @param lookahead ๊ธฐ์กด lookahead + * @param firstSets FIRST ์ง‘ํ•ฉ๋“ค + * @return ๊ณ„์‚ฐ๋œ lookahead ์ง‘ํ•ฉ + */ + private fun calculateLookaheads( + beta: List, + lookahead: Set, + firstSets: Map> + ): Set { + if (beta.isEmpty()) { + return lookahead + } + + val result = mutableSetOf() + var canDeriveEpsilon = true + + for (symbol in beta) { + val firstSet = firstSets[symbol] ?: setOf(symbol) + result.addAll(firstSet.filter { it != TokenType.DOLLAR }) + + if (TokenType.DOLLAR !in firstSet) { + canDeriveEpsilon = false + break + } + } + + if (canDeriveEpsilon) { + result.addAll(lookahead) + } + + return result + } + + /** + * ์ƒํƒœ ๋ฐ์ดํ„ฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒํƒœ ID + * @param items LR ์•„์ดํ…œ๋“ค + */ + private fun validateStateData(id: Int, items: Set) { + if (id < 0) { + throw ParserException.invalidStateId(id) + } + + if (items.isEmpty()) { + throw ParserException.emptyStateItems() + } + + if (items.size > MAX_ITEMS_PER_STATE) { + throw ParserException.itemsPerStateExceedsLimit(items.size, MAX_ITEMS_PER_STATE) + } + } + + /** + * ์•ก์…˜ ํ…Œ์ด๋ธ”์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param actions ์•ก์…˜ ํ…Œ์ด๋ธ” + */ + private fun validateActions(actions: Map) { + actions.forEach { (terminal, _) -> + if (!terminal.isTerminal) { + throw ParserException.actionTableContainsNonTerminal(terminal) + } + } + } + + /** + * Goto ํ…Œ์ด๋ธ”์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param gotos Goto ํ…Œ์ด๋ธ” + */ + private fun validateGotos(gotos: Map) { + gotos.forEach { (nonTerminal, targetState) -> + if (!nonTerminal.isNonTerminal()) { + throw ParserException.gotoTableContainsTerminal(nonTerminal) + } + + if (targetState < 0) { + throw ParserException.targetStateNegative(targetState) + } + } + } + + /** + * ์ „์ด ํ…Œ์ด๋ธ”์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param transitions ์ „์ด ํ…Œ์ด๋ธ” + */ + private fun validateTransitions(transitions: Map) { + if (transitions.size > MAX_TRANSITIONS_PER_STATE) { + throw ParserException.transitionsPerStateExceedsLimit( + transitions.size, MAX_TRANSITIONS_PER_STATE + ) + } + + transitions.forEach { (_, targetState) -> + if (targetState < 0) { + throw ParserException.targetStateNegative(targetState) + } + } + } + + /** + * ํŒฉํ† ๋ฆฌ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxStateCount" to MAX_STATE_COUNT, + "maxItemsPerState" to MAX_ITEMS_PER_STATE, + "maxTransitionsPerState" to MAX_TRANSITIONS_PER_STATE, + "nextStateId" to nextStateId, + "supportedOperations" to listOf( + "createParsingState", "createInitialState", "createAcceptingState", + "createErrorState", "createStateWithClosure", "createStateWithGoto", + "createStateWithActions", "createStateWithGotos", "createCompleteState", + "mergeStates", "copyState", "modifyStateItems" + ) + ) + + /** + * ํŒฉํ† ๋ฆฌ ์‚ฌ์šฉ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "factoryName" to "ParsingStateFactory", + "creationMethods" to 11, + "currentNextId" to nextStateId, + "utilizationRatio" to (nextStateId.toDouble() / MAX_STATE_COUNT) + ) + + /** + * ๋‹ค์Œ ID ์นด์šดํ„ฐ๋ฅผ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param startId ์‹œ์ž‘ ID + */ + fun resetIdCounter(startId: Int = 0) { + nextStateId = startId + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ProductionFactory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ProductionFactory.kt new file mode 100644 index 00000000..759db0b9 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/factories/ProductionFactory.kt @@ -0,0 +1,513 @@ +package hs.kr.entrydsm.domain.parser.factories + +import hs.kr.entrydsm.domain.ast.factory.ASTBuilders +import hs.kr.entrydsm.domain.ast.factory.ASTBuilderContract +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.type.Complexity + +/** + * Production(์ƒ์‚ฐ ๊ทœ์น™) ์ƒ์„ฑ์„ ๋‹ด๋‹นํ•˜๋Š” ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Factory ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์ƒ์‚ฐ ๊ทœ์น™์˜ ๋ณต์žกํ•œ ์ƒ์„ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ , + * ๋‹ค์–‘ํ•œ ์œ ํ˜•์˜ ์ƒ์‚ฐ ๊ทœ์น™ ์ƒ์„ฑ ๋ฐฉ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. BNF ๋ฌธ๋ฒ• ๊ทœ์น™์„ + * Production ๊ฐ์ฒด๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ณผ์ •์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Factory( + context = "parser", + complexity = Complexity.NORMAL, + cache = true +) +class ProductionFactory { + + companion object { + private const val MAX_PRODUCTION_LENGTH = 50 + private const val MAX_PRODUCTION_COUNT = 1000 + private var nextProductionId = 0 + } + + /** + * ๊ธฐ๋ณธ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param right ์šฐ๋ณ€ ์‹ฌ๋ณผ๋“ค + * @param builder AST ๋นŒ๋” + * @return ์ƒ์„ฑ๋œ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createProduction( + id: Int, + left: TokenType, + right: List, + builder: ASTBuilderContract = ASTBuilders.Identity + ): Production { + validateProductionData(id, left, right) + + return Production( + id = id, + left = left, + right = right, + astBuilder = builder + ) + } + + /** + * ์ž๋™ ID ํ• ๋‹น์œผ๋กœ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param right ์šฐ๋ณ€ ์‹ฌ๋ณผ๋“ค + * @param builder AST ๋นŒ๋” + * @return ์ƒ์„ฑ๋œ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createProduction( + left: TokenType, + right: List, + builder: ASTBuilderContract = ASTBuilders.Identity + ): Production { + val id = generateNextId() + return createProduction(id, left, right, builder) + } + + /** + * ๋‹จ์ผ ์‹ฌ๋ณผ ์šฐ๋ณ€์„ ๊ฐ€์ง„ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param right ๋‹จ์ผ ์šฐ๋ณ€ ์‹ฌ๋ณผ + * @param builder AST ๋นŒ๋” + * @return ์ƒ์„ฑ๋œ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createSingleSymbolProduction( + id: Int, + left: TokenType, + right: TokenType, + builder: ASTBuilderContract = ASTBuilders.Identity + ): Production { + return createProduction(id, left, listOf(right), builder) + } + + /** + * ์—ก์‹ค๋ก  ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param builder AST ๋นŒ๋” + * @return ์—ก์‹ค๋ก  ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createEpsilonProduction( + id: Int, + left: TokenType, + builder: ASTBuilderContract = ASTBuilders.Identity + ): Production { + return createProduction(id, left, emptyList(), builder) + } + + /** + * ์ดํ•ญ ์—ฐ์‚ฐ์ž ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param leftOperand ์ขŒ์ธก ํ”ผ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @param operator ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @param rightOperand ์šฐ์ธก ํ”ผ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @return ์ดํ•ญ ์—ฐ์‚ฐ์ž ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createBinaryOperatorProduction( + id: Int, + left: TokenType, + leftOperand: TokenType, + operator: TokenType, + rightOperand: TokenType + ): Production { + if (!operator.isOperator) { + throw ParserException.notOperatorSymbol(operator) + } + + val operatorSymbol = when (operator) { + TokenType.PLUS -> "+" + TokenType.MINUS -> "-" + TokenType.MULTIPLY -> "*" + TokenType.DIVIDE -> "/" + TokenType.MODULO -> "%" + TokenType.POWER -> "^" + TokenType.AND -> "&&" + TokenType.OR -> "||" + TokenType.EQUAL -> "==" + TokenType.NOT_EQUAL -> "!=" + TokenType.LESS -> "<" + TokenType.LESS_EQUAL -> "<=" + TokenType.GREATER -> ">" + TokenType.GREATER_EQUAL -> ">=" + else -> operator.name + } + + return createProduction( + id = id, + left = left, + right = listOf(leftOperand, operator, rightOperand), + builder = ASTBuilders.createBinaryOp(operatorSymbol) + ) + } + + /** + * ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param operator ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @param operand ํ”ผ์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ + * @return ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createUnaryOperatorProduction( + id: Int, + left: TokenType, + operator: TokenType, + operand: TokenType + ): Production { + if (!operator.isOperator) { + throw ParserException.notOperatorSymbol(operator) + } + + val operatorSymbol = when (operator) { + TokenType.MINUS -> "-" + TokenType.PLUS -> "+" + TokenType.NOT -> "!" + else -> operator.name + } + + return createProduction( + id = id, + left = left, + right = listOf(operator, operand), + builder = ASTBuilders.createUnaryOp(operatorSymbol) + ) + } + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param hasArguments ์ธ์ˆ˜๊ฐ€ ์žˆ๋Š”์ง€ ์—ฌ๋ถ€ + * @return ํ•จ์ˆ˜ ํ˜ธ์ถœ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createFunctionCallProduction( + id: Int, + left: TokenType, + hasArguments: Boolean = true + ): Production { + return if (hasArguments) { + createProduction( + id = id, + left = left, + right = listOf(TokenType.IDENTIFIER, TokenType.LEFT_PAREN, TokenType.ARGS, TokenType.RIGHT_PAREN), + builder = ASTBuilders.FunctionCall + ) + } else { + createProduction( + id = id, + left = left, + right = listOf(TokenType.IDENTIFIER, TokenType.LEFT_PAREN, TokenType.RIGHT_PAREN), + builder = ASTBuilders.FunctionCallEmpty + ) + } + } + + /** + * ์กฐ๊ฑด๋ถ€ ํ‘œํ˜„์‹ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @return ์กฐ๊ฑด๋ถ€ ํ‘œํ˜„์‹ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createConditionalProduction( + id: Int, + left: TokenType + ): Production { + return createProduction( + id = id, + left = left, + right = listOf( + TokenType.IF, TokenType.LEFT_PAREN, TokenType.EXPR, TokenType.COMMA, + TokenType.EXPR, TokenType.COMMA, TokenType.EXPR, TokenType.RIGHT_PAREN + ), + builder = ASTBuilders.If + ) + } + + /** + * ๊ด„ํ˜ธ ํ‘œํ˜„์‹ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param inner ๋‚ด๋ถ€ ํ‘œํ˜„์‹ ์‹ฌ๋ณผ + * @return ๊ด„ํ˜ธ ํ‘œํ˜„์‹ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createParenthesizedProduction( + id: Int, + left: TokenType, + inner: TokenType + ): Production { + return createProduction( + id = id, + left = left, + right = listOf(TokenType.LEFT_PAREN, inner, TokenType.RIGHT_PAREN), + builder = ASTBuilders.Parenthesized + ) + } + + /** + * ํ„ฐ๋ฏธ๋„ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param terminal ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @return ํ„ฐ๋ฏธ๋„ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createTerminalProduction( + id: Int, + left: TokenType, + terminal: TokenType + ): Production { + if (!terminal.isTerminal) { + throw ParserException.notATerminal(terminal) + } + + val builder = when (terminal) { + TokenType.NUMBER -> ASTBuilders.Number + TokenType.IDENTIFIER, TokenType.VARIABLE -> ASTBuilders.Variable + TokenType.TRUE -> ASTBuilders.BooleanTrue + TokenType.FALSE -> ASTBuilders.BooleanFalse + else -> ASTBuilders.Identity + } + + return createProduction(id, left, listOf(terminal), builder) + } + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param isSingle ๋‹จ์ผ ์ธ์ˆ˜์ธ์ง€ ์—ฌ๋ถ€ + * @return ์ธ์ˆ˜ ๋ชฉ๋ก ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createArgumentListProduction( + id: Int, + left: TokenType, + isSingle: Boolean = true + ): Production { + return if (isSingle) { + createProduction( + id = id, + left = left, + right = listOf(TokenType.EXPR), + builder = ASTBuilders.ArgsSingle + ) + } else { + createProduction( + id = id, + left = left, + right = listOf(left, TokenType.COMMA, TokenType.EXPR), + builder = ASTBuilders.ArgsMultiple + ) + } + } + + /** + * ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค (LR ํŒŒ์„œ์šฉ). + * + * @param startSymbol ์‹œ์ž‘ ์‹ฌ๋ณผ + * @param endSymbol ๋ ์‹ฌ๋ณผ + * @return ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createAugmentedProduction( + startSymbol: TokenType, + endSymbol: TokenType = TokenType.DOLLAR + ): Production { + return createProduction( + id = -1, + left = TokenType.START, + right = listOf(startSymbol, endSymbol), + builder = ASTBuilders.Start + ) + } + + /** + * BNF ๋ฌธ์ž์—ด๋กœ๋ถ€ํ„ฐ ์ƒ์‚ฐ ๊ทœ์น™์„ ํŒŒ์‹ฑํ•˜์—ฌ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param bnfRule BNF ํ˜•ํƒœ์˜ ๊ทœ์น™ ๋ฌธ์ž์—ด (์˜ˆ: "EXPR -> EXPR + TERM") + * @return ํŒŒ์‹ฑ๋œ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun createFromBNF(id: Int, bnfRule: String): Production { + val parts = bnfRule.split("->").map { it.trim() } + if (parts.size != 2) { + throw ParserException.invalidBnfFormat(bnfRule = bnfRule, partsCount = parts.size) + } + + val leftSymbol = parseTokenType(parts[0]) + val rightSymbols = if (parts[1].trim() == "ฮต" || parts[1].trim().isEmpty()) { + emptyList() + } else { + parts[1].split("\\s+".toRegex()).map { parseTokenType(it) } + } + + return createProduction(id, leftSymbol, rightSymbols) + } + + /** + * ์—ฌ๋Ÿฌ ์ƒ์‚ฐ ๊ทœ์น™์„ ํ•œ ๋ฒˆ์— ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ์ƒ์„ฑํ•  ์ƒ์‚ฐ ๊ทœ์น™๋“ค์˜ ์ •์˜ + * @return ์ƒ์„ฑ๋œ ์ƒ์‚ฐ ๊ทœ์น™๋“ค + */ + fun createMultipleProductions( + productions: List + ): List { + if (productions.size > MAX_PRODUCTION_COUNT) { + throw ParserException.productionCountExceedsLimit( + count = productions.size, + maxCount = MAX_PRODUCTION_COUNT + ) + } + + return productions.map { definition -> + createProduction( + id = definition.id, + left = definition.left, + right = definition.right, + builder = definition.builder + ) + } + } + + /** + * ๊ธฐ์กด ์ƒ์‚ฐ ๊ทœ์น™์„ ๋ณต์‚ฌํ•˜์—ฌ ์ƒˆ๋กœ์šด ID๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param original ์›๋ณธ ์ƒ์‚ฐ ๊ทœ์น™ + * @param newId ์ƒˆ๋กœ์šด ID + * @return ๋ณต์‚ฌ๋œ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun copyProduction(original: Production, newId: Int): Production { + return createProduction( + id = newId, + left = original.left, + right = original.right, + builder = original.astBuilder + ) + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ์šฐ๋ณ€์„ ์ˆ˜์ •ํ•œ ์ƒˆ๋กœ์šด ๊ทœ์น™์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param original ์›๋ณธ ์ƒ์‚ฐ ๊ทœ์น™ + * @param newRight ์ƒˆ๋กœ์šด ์šฐ๋ณ€ + * @param newId ์ƒˆ๋กœ์šด ID (null์ด๋ฉด ์ž๋™ ์ƒ์„ฑ) + * @return ์ˆ˜์ •๋œ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun modifyProductionRight( + original: Production, + newRight: List, + newId: Int? = null + ): Production { + val id = newId ?: generateNextId() + return createProduction(id, original.left, newRight, original.astBuilder) + } + + /** + * ๋‹ค์Œ ์ƒ์‚ฐ ๊ทœ์น™ ID๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹ค์Œ ID + */ + private fun generateNextId(): Int { + return nextProductionId++ + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™ ๋ฐ์ดํ„ฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param left ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @param right ์šฐ๋ณ€ ์‹ฌ๋ณผ๋“ค + */ + private fun validateProductionData(id: Int, left: TokenType, right: List) { + if (!left.isNonTerminal()) { + throw ParserException.productionLeftNotNonTerminal(left) + } + if (right.size > MAX_PRODUCTION_LENGTH) { + throw ParserException.productionLengthExceedsLimit( + length = right.size, + maxLength = MAX_PRODUCTION_LENGTH + ) + } + } + + /** + * ๋ฌธ์ž์—ด์„ TokenType์œผ๋กœ ํŒŒ์‹ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenString ํ† ํฐ ๋ฌธ์ž์—ด + * @return ํŒŒ์‹ฑ๋œ TokenType + */ + private fun parseTokenType(tokenString: String): TokenType { + return try { + TokenType.valueOf(tokenString.uppercase()) + } catch (e: IllegalArgumentException) { + throw ParserException.unknownTokenType(tokenString) + } + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™ ์ •์˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class ProductionDefinition( + val id: Int, + val left: TokenType, + val right: List, + val builder: ASTBuilderContract = ASTBuilders.Identity + ) + + /** + * ํŒฉํ† ๋ฆฌ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxProductionLength" to MAX_PRODUCTION_LENGTH, + "maxProductionCount" to MAX_PRODUCTION_COUNT, + "nextProductionId" to nextProductionId, + "supportedOperations" to listOf( + "createProduction", "createBinaryOperatorProduction", "createUnaryOperatorProduction", + "createFunctionCallProduction", "createConditionalProduction", "createTerminalProduction", + "createFromBNF", "createMultipleProductions", "copyProduction" + ) + ) + + /** + * ํŒฉํ† ๋ฆฌ ์‚ฌ์šฉ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "factoryName" to "ProductionFactory", + "creationMethods" to 15, + "currentNextId" to nextProductionId, + "specializedBuilders" to 8 + ) + + /** + * ๋‹ค์Œ ID ์นด์šดํ„ฐ๋ฅผ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param startId ์‹œ์ž‘ ID + */ + fun resetIdCounter(startId: Int = 0) { + nextProductionId = startId + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/interfaces/GrammarProvider.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/interfaces/GrammarProvider.kt new file mode 100644 index 00000000..507714c0 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/interfaces/GrammarProvider.kt @@ -0,0 +1,309 @@ +package hs.kr.entrydsm.domain.parser.interfaces + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.Production + +/** + * ๋ฌธ๋ฒ• ์ •๋ณด ์ œ๊ณต์„ ๋‹ด๋‹นํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Anti-Corruption Layer ์—ญํ• ์„ ์ˆ˜ํ–‰ํ•˜์—ฌ ๋ฌธ๋ฒ• ๊ด€๋ จ ์ •๋ณด๋ฅผ + * ํ‘œ์ค€ํ™”๋œ ๋ฐฉ์‹์œผ๋กœ ์ œ๊ณตํ•˜๋ฉฐ, ๋‹ค์–‘ํ•œ ๋ฌธ๋ฒ• ๊ตฌํ˜„์ฒด๋“ค ๊ฐ„์˜ + * ํ˜ธํ™˜์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. DDD ์ธํ„ฐํŽ˜์ด์Šค ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ + * ๊ตฌํ˜„์ฒด์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์˜ ๊ฒฐํ•ฉ๋„๋ฅผ ๋‚ฎ์ถฅ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface GrammarProvider { + + /** + * ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์‚ฐ ๊ทœ์น™ ๋ชฉ๋ก + */ + fun getProductions(): List + + /** + * ํ™•์žฅ๋œ ์ƒ์‚ฐ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค (LR ํŒŒ์„œ์šฉ). + * + * @return ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™ + */ + fun getAugmentedProduction(): Production + + /** + * ์‹œ์ž‘ ์‹ฌ๋ณผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹œ์ž‘ ์‹ฌ๋ณผ + */ + fun getStartSymbol(): TokenType + + /** + * ๋ชจ๋“  ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + */ + fun getTerminals(): Set + + /** + * ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + */ + fun getNonTerminals(): Set + + /** + * ํŠน์ • ID์˜ ์ƒ์‚ฐ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ƒ์‚ฐ ๊ทœ์น™ ID + * @return ํ•ด๋‹น ์ƒ์‚ฐ ๊ทœ์น™ + * @throws IllegalArgumentException ID๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun getProductionById(id: Int): Production + + /** + * ํŠน์ • ์ขŒ๋ณ€์„ ๊ฐ€์ง„ ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param leftSymbol ์ขŒ๋ณ€ ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ์ขŒ๋ณ€์„ ๊ฐ€์ง„ ์ƒ์‚ฐ ๊ทœ์น™๋“ค + */ + fun getProductionsFor(leftSymbol: TokenType): List + + /** + * ํŠน์ • ์‹ฌ๋ณผ์„ ํฌํ•จํ•˜๋Š” ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ํฌํ•จํ•  ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ์‹ฌ๋ณผ์„ ํฌํ•จํ•˜๋Š” ์ƒ์‚ฐ ๊ทœ์น™๋“ค + */ + fun getProductionsContaining(symbol: TokenType): List + + /** + * ์ขŒ์žฌ๊ท€ ์ƒ์‚ฐ ๊ทœ์น™๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ขŒ์žฌ๊ท€ ์ƒ์‚ฐ ๊ทœ์น™๋“ค + */ + fun getLeftRecursiveProductions(): List + + /** + * ์šฐ์žฌ๊ท€ ์ƒ์‚ฐ ๊ทœ์น™๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์žฌ๊ท€ ์ƒ์‚ฐ ๊ทœ์น™๋“ค + */ + fun getRightRecursiveProductions(): List + + /** + * ์—ก์‹ค๋ก  ์ƒ์‚ฐ ๊ทœ์น™๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ก์‹ค๋ก  ์ƒ์‚ฐ ๊ทœ์น™๋“ค + */ + fun getEpsilonProductions(): List + + /** + * ํŠน์ • ์‹ฌ๋ณผ์ด ํ„ฐ๋ฏธ๋„์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ํ™•์ธํ•  ์‹ฌ๋ณผ + * @return ํ„ฐ๋ฏธ๋„์ด๋ฉด true + */ + fun isTerminal(symbol: TokenType): Boolean + + /** + * ํŠน์ • ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ํ™•์ธํ•  ์‹ฌ๋ณผ + * @return ๋…ผํ„ฐ๋ฏธ๋„์ด๋ฉด true + */ + fun isNonTerminal(symbol: TokenType): Boolean + + /** + * ๋ฌธ๋ฒ•์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isValid(): Boolean + + /** + * ๋ฌธ๋ฒ•์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getGrammarStatistics(): Map + + /** + * ๋ฌธ๋ฒ•์„ BNF ํ˜•ํƒœ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @return BNF ํ˜•ํƒœ์˜ ๋ฌธ๋ฒ• ๋ฌธ์ž์—ด + */ + fun toBNFString(): String + + /** + * ๋ฌธ๋ฒ•์˜ ๊ฐ„๋‹จํ•œ ์š”์•ฝ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฌธ๋ฒ• ์š”์•ฝ ๋ฌธ์ž์—ด + */ + fun getSummary(): String + + /** + * ํŠน์ • ๋…ผํ„ฐ๋ฏธ๋„์˜ ์ƒ์‚ฐ ๊ทœ์น™๋“ค์„ BNF ํ˜•ํƒœ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param nonTerminal ์ถœ๋ ฅํ•  ๋…ผํ„ฐ๋ฏธ๋„ + * @return BNF ํ˜•ํƒœ์˜ ์ƒ์‚ฐ ๊ทœ์น™ ๋ฌธ์ž์—ด + */ + fun getProductionsBNF(nonTerminal: TokenType): String + + /** + * ๋ฌธ๋ฒ•์˜ ๋ณต์žก๋„๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณต์žก๋„ ์ง€์ˆ˜ + */ + fun calculateComplexity(): Int { + val productionCount = getProductions().size + val terminalCount = getTerminals().size + val nonTerminalCount = getNonTerminals().size + val avgProductionLength = getProductions().map { it.right.size }.average() + + return (productionCount * 1.0 + + terminalCount * 0.5 + + nonTerminalCount * 2.0 + + avgProductionLength * 1.5).toInt() + } + + /** + * ๋ฌธ๋ฒ•์ด LR(k) ๋ฌธ๋ฒ•์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param k LR ๋ ˆ๋ฒจ + * @return LR(k) ๋ฌธ๋ฒ•์ด๋ฉด true + */ + fun isLRGrammar(k: Int = 1): Boolean { + // ๊ธฐ๋ณธ ๊ตฌํ˜„: ๊ฐ„๋‹จํ•œ ๊ฒ€์‚ฌ + return isValid() && + getLeftRecursiveProductions().isEmpty() && + getEpsilonProductions().size <= getNonTerminals().size * 0.3 + } + + /** + * ๋ฌธ๋ฒ•์ด LALR(1) ๋ฌธ๋ฒ•์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return LALR(1) ๋ฌธ๋ฒ•์ด๋ฉด true + */ + fun isLALRGrammar(): Boolean { + return isLRGrammar(1) && + getProductions().size <= 1000 // ์‹ค์šฉ์ ์ธ ํฌ๊ธฐ ์ œํ•œ + } + + /** + * ๋ฌธ๋ฒ•์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋งต + */ + fun getMetadata(): Map = mapOf( + "grammarType" to "Context-Free", + "parsingStrategy" to "LR(1)", + "complexity" to calculateComplexity(), + "isLR" to isLRGrammar(), + "isLALR" to isLALRGrammar(), + "hasLeftRecursion" to getLeftRecursiveProductions().isNotEmpty(), + "hasEpsilonProductions" to getEpsilonProductions().isNotEmpty(), + "maxProductionLength" to (getProductions().maxOfOrNull { it.right.size } ?: 0), + "averageProductionLength" to (getProductions().map { it.right.size }.average()) + ) + + /** + * ๋‘ ๋ฌธ๋ฒ•์ด ๋™๋“ฑํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๋ฌธ๋ฒ• ์ œ๊ณต์ž + * @return ๋™๋“ฑํ•˜๋ฉด true + */ + fun isEquivalentTo(other: GrammarProvider): Boolean { + return getStartSymbol() == other.getStartSymbol() && + getTerminals() == other.getTerminals() && + getNonTerminals() == other.getNonTerminals() && + getProductions().toSet() == other.getProductions().toSet() + } + + /** + * ๋ฌธ๋ฒ•์„ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ตœ์ ํ™”๋œ ๋ฌธ๋ฒ• ์ œ๊ณต์ž + */ + fun optimize(): GrammarProvider { + // ๊ธฐ๋ณธ ๊ตฌํ˜„: ์ž๊ธฐ ์ž์‹  ๋ฐ˜ํ™˜ + return this + } + + /** + * ๋ฌธ๋ฒ•์˜ ๋ฌด๊ฒฐ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ์™€ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋“ค + */ + fun validateIntegrity(): ValidationResult { + val errors = mutableListOf() + val warnings = mutableListOf() + + // ๊ธฐ๋ณธ ๊ฒ€์ฆ + if (getProductions().isEmpty()) { + errors.add("์ƒ์‚ฐ ๊ทœ์น™์ด ์—†์Šต๋‹ˆ๋‹ค") + } + + if (getTerminals().isEmpty()) { + errors.add("ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์—†์Šต๋‹ˆ๋‹ค") + } + + if (getNonTerminals().isEmpty()) { + errors.add("๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์—†์Šต๋‹ˆ๋‹ค") + } + + // ์‹œ์ž‘ ์‹ฌ๋ณผ ๊ฒ€์ฆ + if (!isNonTerminal(getStartSymbol())) { + errors.add("์‹œ์ž‘ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ด ์•„๋‹™๋‹ˆ๋‹ค: ${getStartSymbol()}") + } + + // ์ƒ์‚ฐ ๊ทœ์น™ ๊ฒ€์ฆ + getProductions().forEach { production -> + if (!isNonTerminal(production.left)) { + errors.add("์ƒ์‚ฐ ๊ทœ์น™ ${production.id}์˜ ์ขŒ๋ณ€์ด ๋…ผํ„ฐ๋ฏธ๋„์ด ์•„๋‹™๋‹ˆ๋‹ค: ${production.left}") + } + + production.right.forEach { symbol -> + if (!isTerminal(symbol) && !isNonTerminal(symbol)) { + errors.add("์ƒ์‚ฐ ๊ทœ์น™ ${production.id}์— ์ •์˜๋˜์ง€ ์•Š์€ ์‹ฌ๋ณผ์ด ์žˆ์Šต๋‹ˆ๋‹ค: $symbol") + } + } + } + + // ๊ฒฝ๊ณ  ๊ฒ€์‚ฌ + if (getLeftRecursiveProductions().isNotEmpty()) { + warnings.add("์ขŒ์žฌ๊ท€ ์ƒ์‚ฐ ๊ทœ์น™์ด ์žˆ์Šต๋‹ˆ๋‹ค: ${getLeftRecursiveProductions().size}๊ฐœ") + } + + return ValidationResult( + isValid = errors.isEmpty(), + errors = errors, + warnings = warnings + ) + } + + /** + * ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class ValidationResult( + val isValid: Boolean, + val errors: List, + val warnings: List + ) { + fun hasErrors(): Boolean = errors.isNotEmpty() + fun hasWarnings(): Boolean = warnings.isNotEmpty() + + override fun toString(): String = buildString { + appendLine("๋ฌธ๋ฒ• ๊ฒ€์ฆ ๊ฒฐ๊ณผ: ${if (isValid) "์œ ํšจ" else "์˜ค๋ฅ˜"}") + if (errors.isNotEmpty()) { + appendLine("์˜ค๋ฅ˜:") + errors.forEach { appendLine(" - $it") } + } + if (warnings.isNotEmpty()) { + appendLine("๊ฒฝ๊ณ :") + warnings.forEach { appendLine(" - $it") } + } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/interfaces/ParserContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/interfaces/ParserContract.kt new file mode 100644 index 00000000..dd12842d --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/interfaces/ParserContract.kt @@ -0,0 +1,192 @@ +package hs.kr.entrydsm.domain.parser.interfaces + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.parser.values.ParsingResult + +/** + * Parser ๋„๋ฉ”์ธ์˜ ํ•ต์‹ฌ ๊ณ„์•ฝ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ๊ตฌ๋ฌธ ๋ถ„์„๊ธฐ(Parser)๊ฐ€ ์ œ๊ณตํ•ด์•ผ ํ•˜๋Š” ๊ธฐ๋ณธ ๊ธฐ๋Šฅ๋“ค์„ ๋ช…์„ธํ•˜๋ฉฐ, + * ๋‹ค์–‘ํ•œ ๊ตฌํ˜„์ฒด๋“ค์ด ๋”ฐ๋ผ์•ผ ํ•˜๋Š” ํ‘œ์ค€ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * Anti-Corruption Layer ์—ญํ• ๋„ ์ˆ˜ํ–‰ํ•˜์—ฌ ์™ธ๋ถ€ ๋„๋ฉ”์ธ๊ณผ์˜ + * ์˜์กด์„ฑ์„ ๊ฒฉ๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface ParserContract { + + /** + * ํ† ํฐ ๋ชฉ๋ก์„ ๊ตฌ๋ฌธ ๋ถ„์„ํ•˜์—ฌ AST๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ตฌ๋ฌธ ๋ถ„์„ํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ (AST ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ) + */ + fun parse(tokens: List): ParsingResult + + /** + * ๋‹จ์ผ ํ† ํฐ ์ŠคํŠธ๋ฆผ์„ ๊ตฌ๋ฌธ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenSequence ํ† ํฐ ์‹œํ€€์Šค + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + fun parseSequence(tokenSequence: Sequence): ParsingResult + + /** + * ์ฃผ์–ด์ง„ ํ† ํฐ ๋ชฉ๋ก์ด ๋ฌธ๋ฒ•์ ์œผ๋กœ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ์œ ํšจํ•˜๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + */ + fun validate(tokens: List): Boolean + + /** + * ๋ถ€๋ถ„ ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ตฌ๋ฌธ ์™„์„ฑ, ์—๋Ÿฌ ๋ณต๊ตฌ ๋“ฑ์— ์‚ฌ์šฉ). + * + * @param tokens ๋ถ€๋ถ„ ํ† ํฐ ๋ชฉ๋ก + * @param allowIncomplete ๋ถˆ์™„์ „ํ•œ ๊ตฌ๋ฌธ ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @return ๋ถ€๋ถ„ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + fun parsePartial(tokens: List, allowIncomplete: Boolean = true): ParsingResult + + /** + * ๋‹ค์Œ์— ์˜ฌ ์ˆ˜ ์žˆ๋Š” ์œ ํšจํ•œ ํ† ํฐ๋“ค์„ ์˜ˆ์ธกํ•ฉ๋‹ˆ๋‹ค. + * + * @param currentTokens ํ˜„์žฌ๊นŒ์ง€์˜ ํ† ํฐ ๋ชฉ๋ก + * @return ๋‹ค์Œ์— ์˜ฌ ์ˆ˜ ์žˆ๋Š” ํ† ํฐ ํƒ€์ž…๋“ค + */ + fun predictNextTokens(currentTokens: List): Set + + /** + * ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์œ„์น˜์™€ ์˜ˆ์ƒ ํ† ํฐ์„ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๋ถ„์„ํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ์˜ค๋ฅ˜ ๋ถ„์„ ๊ฒฐ๊ณผ + */ + fun analyzeErrors(tokens: List): Map + + /** + * ํŒŒ์„œ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์„œ ์ƒํƒœ ์ •๋ณด + */ + fun getState(): Map + + /** + * ํŒŒ์„œ๋ฅผ ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun reset() + + /** + * ํŒŒ์„œ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map + + /** + * ํŒŒ์‹ฑ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต (ํŒŒ์‹ฑ ํšŸ์ˆ˜, ์„ฑ๊ณต๋ฅ , ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๋“ฑ) + */ + fun getStatistics(): Map + + /** + * ๋””๋ฒ„๊ทธ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ๋””๋ฒ„๊ทธ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + fun setDebugMode(enabled: Boolean) + + /** + * ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + fun setErrorRecoveryMode(enabled: Boolean) + + /** + * ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxDepth ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด + */ + fun setMaxParsingDepth(maxDepth: Int) + + /** + * ์ŠคํŠธ๋ฆฌ๋ฐ ๋ชจ๋“œ๋กœ ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ํ† ํฐ ๋ชฉ๋ก + * @param callback ํŒŒ์‹ฑ ์ง„ํ–‰ ์ƒํ™ฉ ์ฝœ๋ฐฑ + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + fun parseStreaming(tokens: List, callback: (progress: Double) -> Unit): ParsingResult + + /** + * ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ตฌ๋ฌธ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๋ถ„์„ํ•  ํ† ํฐ ๋ชฉ๋ก + * @param callback ๋ถ„์„ ์™„๋ฃŒ ์‹œ ํ˜ธ์ถœ๋  ์ฝœ๋ฐฑ ํ•จ์ˆ˜ + */ + fun parseAsync(tokens: List, callback: (ParsingResult) -> Unit) + + /** + * ์ฆ๋ถ„ ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * ๊ธฐ์กด ํŒŒ์‹ฑ ๊ฒฐ๊ณผ๋ฅผ ์žฌํ™œ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @param previousResult ์ด์ „ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + * @param newTokens ์ƒˆ๋กœ์šด ํ† ํฐ ๋ชฉ๋ก + * @param changeStartIndex ๋ณ€๊ฒฝ ์‹œ์ž‘ ์œ„์น˜ + * @return ์ฆ๋ถ„ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + fun incrementalParse( + previousResult: ParsingResult, + newTokens: List, + changeStartIndex: Int + ): ParsingResult + + /** + * ๋ฌธ๋ฒ• ๊ทœ์น™์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฌธ๋ฒ•์ด ์œ ํšจํ•˜๋ฉด true + */ + fun validateGrammar(): Boolean + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ์ถฉ๋Œ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ถฉ๋Œ ์ •๋ณด ๋งต + */ + fun checkParsingConflicts(): Map + + /** + * ํŠน์ • ์œ„์น˜์—์„œ์˜ ํŒŒ์‹ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenIndex ํ† ํฐ ์ธ๋ฑ์Šค + * @return ํŒŒ์‹ฑ ์ปจํ…์ŠคํŠธ ์ •๋ณด + */ + fun getParsingContext(tokenIndex: Int): Map + + /** + * ํ˜„์žฌ ํŒŒ์‹ฑ ์Šคํƒ์˜ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์‹ฑ ์Šคํƒ ์ •๋ณด + */ + fun getParsingStack(): List + + /** + * ํŒŒ์„œ๊ฐ€ ์ง€์›ํ•˜๋Š” ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜ + */ + fun getMaxSupportedTokens(): Int + + /** + * ํŒŒ์„œ์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ •๋ณด + */ + fun getMemoryUsage(): Map +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/policies/ConflictResolutionPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/policies/ConflictResolutionPolicy.kt new file mode 100644 index 00000000..45438135 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/policies/ConflictResolutionPolicy.kt @@ -0,0 +1,336 @@ +package hs.kr.entrydsm.domain.parser.policies + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.ParsingState +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.values.Associativity +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.parser.services.ConflictResolutionResult +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ํŒŒ์‹ฑ ์ถฉ๋Œ ํ•ด๊ฒฐ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Policy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ LR ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š” + * Shift/Reduce ๋ฐ Reduce/Reduce ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•˜๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ + * ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„์™€ ๊ฒฐํ•ฉ์„ฑ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ฒด๊ณ„์ ์ธ + * ์ถฉ๋Œ ํ•ด๊ฒฐ ์ „๋žต์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Policy( + name = "ConflictResolution", + description = "ํŒŒ์‹ฑ ์ถฉ๋Œ ํ•ด๊ฒฐ์„ ์œ„ํ•œ ์šฐ์„ ์ˆœ์œ„ ๋ฐ ๊ฒฐํ•ฉ์„ฑ ๊ธฐ๋ฐ˜ ์ •์ฑ…", + domain = "parser", + scope = Scope.DOMAIN +) +class ConflictResolutionPolicy { + + companion object { + private const val DEFAULT_PRECEDENCE = 0 + private const val MAX_PRECEDENCE_LEVEL = 100 + } + + private val associativityTable = mutableMapOf() + + init { + // ๊ธฐ๋ณธ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๋ฐ ๊ฒฐํ•ฉ์„ฑ ์„ค์ • + initializeDefaultAssociativities() + } + + /** + * Shift/Reduce ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ํŒŒ์‹ฑ ์ƒํƒœ + * @param shiftToken ์‹œํ”„ํŠธํ•  ํ† ํฐ + * @param reduceProductionId ๋ฆฌ๋“€์Šคํ•  ์ƒ์‚ฐ ๊ทœ์น™ ID + * @return ํ•ด๊ฒฐ๋œ ์•ก์…˜ (SHIFT ๋˜๋Š” REDUCE) + */ + fun resolveShiftReduceConflict( + state: ParsingState, + shiftToken: TokenType, + reduceProduction: hs.kr.entrydsm.domain.parser.entities.Production + ): ConflictResolutionResult { + val shiftPrecedence = getTokenPrecedence(shiftToken) + val reducePrecedence = getProductionPrecedence(reduceProduction.id) + + return when { + shiftPrecedence > reducePrecedence -> { + ConflictResolutionResult.Resolved( + LRAction.Shift(state.id), + "Shift has higher precedence ($shiftPrecedence > $reducePrecedence)" + ) + } + shiftPrecedence < reducePrecedence -> { + ConflictResolutionResult.Resolved( + LRAction.Reduce(reduceProduction), + "Reduce has higher precedence ($reducePrecedence > $shiftPrecedence)" + ) + } + else -> { + // ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๊ฐ™์œผ๋ฉด ๊ฒฐํ•ฉ์„ฑ์œผ๋กœ ํŒ๋‹จ + resolveByAssociativity(state, shiftToken, reduceProduction) + } + } + } + + /** + * Reduce/Reduce ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ํŒŒ์‹ฑ ์ƒํƒœ + * @param productionId1 ์ฒซ ๋ฒˆ์งธ ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param productionId2 ๋‘ ๋ฒˆ์งธ ์ƒ์‚ฐ ๊ทœ์น™ ID + * @param lookahead ์ „๋ฐฉํƒ์ƒ‰ ํ† ํฐ + * @return ํ•ด๊ฒฐ๋œ ์ƒ์‚ฐ ๊ทœ์น™ ID + */ + fun resolveReduceReduceConflict( + state: ParsingState, + production1: hs.kr.entrydsm.domain.parser.entities.Production, + production2: hs.kr.entrydsm.domain.parser.entities.Production, + lookahead: TokenType + ): ConflictResolutionResult { + val precedence1 = getProductionPrecedence(production1.id) + val precedence2 = getProductionPrecedence(production2.id) + + return when { + precedence1 > precedence2 -> { + ConflictResolutionResult.Resolved( + LRAction.Reduce(production1), + "Production ${production1.id} has higher precedence ($precedence1 > $precedence2)" + ) + } + precedence1 < precedence2 -> { + ConflictResolutionResult.Resolved( + LRAction.Reduce(production2), + "Production ${production2.id} has higher precedence ($precedence2 > $precedence1)" + ) + } + else -> { + // ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๊ฐ™์œผ๋ฉด ๋” ๋‚ฎ์€ ID ์„ ํƒ (์ •์˜๋œ ์ˆœ์„œ ์šฐ์„ ) + val earlierProduction = if (production1.id < production2.id) production1 else production2 + ConflictResolutionResult.Resolved( + LRAction.Reduce(earlierProduction), + "Same precedence, choosing earlier defined production (ID: ${earlierProduction.id})" + ) + } + } + } + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ์ด ๊ฐ€๋Šฅํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param conflictType ์ถฉ๋Œ ํƒ€์ž… ("shift_reduce" ๋˜๋Š” "reduce_reduce") + * @param tokens ๊ด€๋ จ ํ† ํฐ๋“ค + * @param productions ๊ด€๋ จ ์ƒ์‚ฐ ๊ทœ์น™ ID๋“ค + * @return ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun canResolveConflict( + conflictType: String, + tokens: Set, + productions: Set + ): Boolean { + return when (conflictType) { + "shift_reduce" -> { + tokens.all { hasDefinedPrecedence(it) } || + productions.all { hasDefinedProductionPrecedence(it) } + } + "reduce_reduce" -> { + productions.all { hasDefinedProductionPrecedence(it) } || + productions.size <= 2 // ์ตœ๋Œ€ 2๊ฐœ ์ƒ์‚ฐ ๊ทœ์น™๋งŒ ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ + } + else -> false + } + } + + /** + * ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๋ฐ ๊ฒฐํ•ฉ์„ฑ์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenType ํ† ํฐ ํƒ€์ž… + * @param associativity ๊ฒฐํ•ฉ์„ฑ ์ •๋ณด + */ + fun setAssociativity(tokenType: TokenType, associativity: Associativity) { + if (associativity.operator != tokenType) { + throw ParserException.associativityOperatorMismatch( + expected = associativity.operator, + actual = tokenType + ) + } + associativityTable[tokenType] = associativity + } + + /** + * ์—ฌ๋Ÿฌ ์—ฐ์‚ฐ์ž์˜ ์šฐ์„ ์ˆœ์œ„ ๋ฐ ๊ฒฐํ•ฉ์„ฑ์„ ์ผ๊ด„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param associativities ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™๋“ค + */ + fun setAssociativities(associativities: List) { + associativities.forEach { associativity -> + setAssociativity(associativity.operator, associativity) + } + } + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ์ „๋žต์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ผ๊ด€์„ฑ์ด ์žˆ์œผ๋ฉด true + */ + fun validateConsistency(): Boolean { + // ์šฐ์„ ์ˆœ์œ„ ๋ ˆ๋ฒจ์ด ์œ ํšจํ•œ ๋ฒ”์œ„ ๋‚ด์— ์žˆ๋Š”์ง€ ํ™•์ธ + val precedences = associativityTable.values.map { it.precedence } + if (precedences.any { it < 0 || it > MAX_PRECEDENCE_LEVEL }) { + return false + } + + // ๋™์ผํ•œ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง„ ์—ฐ์‚ฐ์ž๋“ค์˜ ๊ฒฐํ•ฉ์„ฑ์ด ์ผ๊ด€์„ฑ์ด ์žˆ๋Š”์ง€ ํ™•์ธ + val precedenceGroups = associativityTable.values.groupBy { it.precedence } + precedenceGroups.values.forEach { group -> + if (group.size > 1) { + val associativityTypes = group.map { it.type }.toSet() + if (associativityTypes.size > 1) { + // ๋™์ผํ•œ ์šฐ์„ ์ˆœ์œ„์—์„œ ๋‹ค๋ฅธ ๊ฒฐํ•ฉ์„ฑ์€ ํ—ˆ์šฉํ•˜์ง€ ์•Š์Œ + return false + } + } + } + + return true + } + + /** + * ๊ฒฐํ•ฉ์„ฑ์œผ๋กœ ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun resolveByAssociativity( + state: ParsingState, + shiftToken: TokenType, + reduceProduction: hs.kr.entrydsm.domain.parser.entities.Production + ): ConflictResolutionResult { + val associativity = associativityTable[shiftToken] + + return when (associativity?.type) { + Associativity.AssociativityType.LEFT -> { + ConflictResolutionResult.Resolved( + LRAction.Reduce(reduceProduction), + "Left associative operator: prefer reduce" + ) + } + Associativity.AssociativityType.RIGHT -> { + ConflictResolutionResult.Resolved( + LRAction.Shift(state.id), + "Right associative operator: prefer shift" + ) + } + Associativity.AssociativityType.NONE -> { + ConflictResolutionResult.Unresolved( + "Non-associative operator: conflict cannot be resolved" + ) + } + else -> { + ConflictResolutionResult.Resolved( + LRAction.Shift(state.id), + "No associativity defined: default to shift" + ) + } + } + } + + /** + * ํ† ํฐ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun getTokenPrecedence(token: TokenType): Int { + return associativityTable[token]?.precedence ?: DEFAULT_PRECEDENCE + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun getProductionPrecedence(productionId: Int): Int { + // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„: ์ƒ์‚ฐ ๊ทœ์น™ ID๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์šฐ์„ ์ˆœ์œ„ ๊ณ„์‚ฐ + // ์‹ค์ œ๋กœ๋Š” ์ƒ์‚ฐ ๊ทœ์น™์˜ ๋งˆ์ง€๋ง‰ ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•จ + return when (productionId) { + in 0..10 -> 1 // ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„ (๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž) + in 11..20 -> 5 // ์ค‘๊ฐ„ ์šฐ์„ ์ˆœ์œ„ (์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž) + in 21..30 -> 8 // ๋†’์€ ์šฐ์„ ์ˆœ์œ„ (๋‹จํ•ญ ์—ฐ์‚ฐ์ž) + else -> DEFAULT_PRECEDENCE + } + } + + /** + * ํ† ํฐ์— ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasDefinedPrecedence(token: TokenType): Boolean { + return associativityTable.containsKey(token) + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์— ์šฐ์„ ์ˆœ์œ„๊ฐ€ ์ •์˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasDefinedProductionPrecedence(productionId: Int): Boolean { + return productionId >= 0 // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ + } + + /** + * ๊ธฐ๋ณธ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๋ฐ ๊ฒฐํ•ฉ์„ฑ์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun initializeDefaultAssociativities() { + val defaultRules = listOf( + // ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž (๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„) + Associativity.leftAssoc(TokenType.OR, 1, "๋…ผ๋ฆฌํ•ฉ"), + Associativity.leftAssoc(TokenType.AND, 2, "๋…ผ๋ฆฌ๊ณฑ"), + + // ๋น„๊ต ์—ฐ์‚ฐ์ž + Associativity.leftAssoc(TokenType.EQUAL, 3, "๊ฐ™์Œ"), + Associativity.leftAssoc(TokenType.NOT_EQUAL, 3, "๋‹ค๋ฆ„"), + Associativity.leftAssoc(TokenType.LESS, 4, "๋ฏธ๋งŒ"), + Associativity.leftAssoc(TokenType.LESS_EQUAL, 4, "์ดํ•˜"), + Associativity.leftAssoc(TokenType.GREATER, 4, "์ดˆ๊ณผ"), + Associativity.leftAssoc(TokenType.GREATER_EQUAL, 4, "์ด์ƒ"), + + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž + Associativity.leftAssoc(TokenType.PLUS, 5, "๋ง์…ˆ"), + Associativity.leftAssoc(TokenType.MINUS, 5, "๋บ„์…ˆ"), + Associativity.leftAssoc(TokenType.MULTIPLY, 6, "๊ณฑ์…ˆ"), + Associativity.leftAssoc(TokenType.DIVIDE, 6, "๋‚˜๋ˆ—์…ˆ"), + Associativity.leftAssoc(TokenType.MODULO, 6, "๋‚˜๋จธ์ง€"), + + // ์ง€์ˆ˜ ์—ฐ์‚ฐ์ž (์šฐ๊ฒฐํ•ฉ) + Associativity.rightAssoc(TokenType.POWER, 7, "๊ฑฐ๋“ญ์ œ๊ณฑ"), + + // ๋‹จํ•ญ ์—ฐ์‚ฐ์ž (๊ฐ€์žฅ ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + Associativity.rightAssoc(TokenType.NOT, 8, "๋…ผ๋ฆฌ ๋ถ€์ •") + ) + + setAssociativities(defaultRules) + } + + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "defaultPrecedence" to DEFAULT_PRECEDENCE, + "maxPrecedenceLevel" to MAX_PRECEDENCE_LEVEL, + "associativityTableSize" to associativityTable.size, + "supportedConflictTypes" to listOf("shift_reduce", "reduce_reduce"), + "resolutionStrategies" to listOf("precedence", "associativity", "production_order") + ) + + /** + * ์ •์ฑ…์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "policyName" to "ConflictResolutionPolicy", + "definedOperators" to associativityTable.size, + "precedenceLevels" to associativityTable.values.map { it.precedence }.toSet().size, + "associativityDistribution" to associativityTable.values.groupBy { it.type } + .mapValues { it.value.size }, + "isConsistent" to validateConsistency() + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/policies/GrammarValidationPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/policies/GrammarValidationPolicy.kt new file mode 100644 index 00000000..83caca02 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/policies/GrammarValidationPolicy.kt @@ -0,0 +1,385 @@ +package hs.kr.entrydsm.domain.parser.policies + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * ๋ฌธ๋ฒ• ๊ฒ€์ฆ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Policy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๋ฌธ๋ฒ• ๊ทœ์น™์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ƒ์‚ฐ ๊ทœ์น™์˜ ๊ตฌ์กฐ์  ๋ฌด๊ฒฐ์„ฑ, + * ์ˆœํ™˜ ์ฐธ์กฐ ๊ฒ€์‚ฌ, ๋„๋‹ฌ ๊ฐ€๋Šฅ์„ฑ ๋ถ„์„ ๋“ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Policy( + name = "GrammarValidation", + description = "๋ฌธ๋ฒ• ๊ทœ์น™์˜ ๊ตฌ์กฐ์  ๋ฌด๊ฒฐ์„ฑ๊ณผ ๋…ผ๋ฆฌ์  ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ์ •์ฑ…", + domain = "parser", + scope = Scope.DOMAIN +) +class GrammarValidationPolicy { + + companion object { + private const val MAX_PRODUCTION_COUNT = 1000 + private const val MAX_PRODUCTION_LENGTH = 50 + private const val MAX_RECURSION_DEPTH = 100 + private const val MIN_PRODUCTION_COUNT = 1 + } + + /** + * ๋ฌธ๋ฒ• ์ „์ฒด์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ์ƒ์‚ฐ ๊ทœ์น™ ๋ชฉ๋ก + * @param startSymbol ์‹œ์ž‘ ์‹ฌ๋ณผ + * @param terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateGrammar( + productions: List, + startSymbol: TokenType, + terminals: Set, + nonTerminals: Set + ): Boolean { + validateBasicStructure(productions, startSymbol, terminals, nonTerminals) + validateProductionRules(productions, terminals, nonTerminals) + validateReachability(productions, startSymbol, nonTerminals) + validateCompleteness(productions, nonTerminals) + validateNoLeftRecursion(productions) + + return true + } + + /** + * ๊ฐœ๋ณ„ ์ƒ์‚ฐ ๊ทœ์น™์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param production ๊ฒ€์ฆํ•  ์ƒ์‚ฐ ๊ทœ์น™ + * @param terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateProduction( + production: Production, + terminals: Set, + nonTerminals: Set + ): Boolean { + validateProductionStructure(production) + validateProductionSymbols(production, terminals, nonTerminals) + + return true + } + + /** + * ์ขŒ์žฌ๊ท€๊ฐ€ ์—†๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ์ƒ์‚ฐ ๊ทœ์น™ ๋ชฉ๋ก + * @return ์ขŒ์žฌ๊ท€๊ฐ€ ์—†์œผ๋ฉด true + */ + fun validateNoLeftRecursion(productions: List): Boolean { + val graph = buildDependencyGraph(productions) + + for (nonTerminal in graph.keys) { + if (hasLeftRecursion(nonTerminal, graph, mutableSetOf())) { + throw ParserException.leftRecursionDetected(nonTerminal) + } + } + + return true + } + + /** + * ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„์ด ๋„๋‹ฌ ๊ฐ€๋Šฅํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ์ƒ์‚ฐ ๊ทœ์น™ ๋ชฉ๋ก + * @param startSymbol ์‹œ์ž‘ ์‹ฌ๋ณผ + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„์ด ๋„๋‹ฌ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun validateReachability( + productions: List, + startSymbol: TokenType, + nonTerminals: Set + ): Boolean { + val reachable = findReachableSymbols(productions, startSymbol) + val unreachable = nonTerminals - reachable + + if (unreachable.isNotEmpty()) { + throw ParserException.unreachableNonTerminals(unreachable) + } + + return true + } + + /** + * ๋ฌธ๋ฒ•์˜ ์™„์ „์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค (๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„์— ๋Œ€ํ•œ ์ƒ์‚ฐ ๊ทœ์น™ ์กด์žฌ). + * + * @param productions ์ƒ์‚ฐ ๊ทœ์น™ ๋ชฉ๋ก + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ์™„์ „ํ•˜๋ฉด true + */ + fun validateCompleteness( + productions: List, + nonTerminals: Set + ): Boolean { + val defined = productions.map { it.left }.toSet() + val undefined = nonTerminals - defined + + if (undefined.isNotEmpty()) { + throw ParserException.undefinedNonTerminals(undefined) + } + + return true + } + + /** + * ๋ฌธ๋ฒ•์— ๋ชจํ˜ธ์„ฑ์ด ์—†๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ์ƒ์‚ฐ ๊ทœ์น™ ๋ชฉ๋ก + * @return ๋ชจํ˜ธ์„ฑ์ด ์—†์œผ๋ฉด true + */ + fun validateUnambiguity(productions: List): Boolean { + // ๊ฐ„๋‹จํ•œ ๋ชจํ˜ธ์„ฑ ๊ฒ€์‚ฌ: ๊ฐ™์€ ์ขŒ๋ณ€์„ ๊ฐ€์ง„ ๊ทœ์น™๋“ค์˜ ์šฐ๋ณ€ ์‹œ์ž‘ ์‹ฌ๋ณผ ์ค‘๋ณต ๊ฒ€์‚ฌ + val groupedByLeft = productions.groupBy { it.left } + + for ((left, rules) in groupedByLeft) { + if (rules.size > 1) { + val firstSymbols = rules.mapNotNull { it.right.firstOrNull() } + val duplicates = firstSymbols.groupBy { it }.filter { it.value.size > 1 } + + if (duplicates.isNotEmpty()) { + throw ParserException.ambiguousGrammarRule(left, duplicates.keys) + } + } + } + + return true + } + + /** + * ์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ์—†๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ์ƒ์‚ฐ ๊ทœ์น™ ๋ชฉ๋ก + * @return ์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ์—†์œผ๋ฉด true + */ + fun validateNoCycles(productions: List): Boolean { + val graph = buildDependencyGraph(productions) + + for (start in graph.keys) { + if (hasCycle(start, graph, mutableSetOf(), mutableSetOf())) { + throw ParserException.cyclicGrammarReference(start) + } + } + + return true + } + + /** + * ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateBasicStructure( + productions: List, + startSymbol: TokenType, + terminals: Set, + nonTerminals: Set + ) { + if (productions.size < MIN_PRODUCTION_COUNT) { + throw ParserException.productionCountBelowMin(productions.size, MIN_PRODUCTION_COUNT) + } + if (productions.size > MAX_PRODUCTION_COUNT) { + throw ParserException.productionCountExceedsLimit(productions.size, MAX_PRODUCTION_COUNT) + } + + if (startSymbol !in nonTerminals) { + throw ParserException.startSymbolNotInNonTerminals(startSymbol) + } + + val overlap = terminals.intersect(nonTerminals) + if (overlap.isNotEmpty()) { + throw ParserException.terminalsAndNonTerminalsOverlap(overlap) + } + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™๋“ค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateProductionRules( + productions: List, + terminals: Set, + nonTerminals: Set + ) { + val allSymbols = terminals + nonTerminals + + productions.forEach { production -> + validateProduction(production, terminals, nonTerminals) + } + + // ์ค‘๋ณต ์ƒ์‚ฐ ๊ทœ์น™ ๊ฒ€์‚ฌ + val duplicates = productions.groupBy { "${it.left}->${it.right.joinToString(",")}" } + .filter { it.value.size > 1 } + + if (duplicates.isNotEmpty()) { + throw ParserException.duplicateProductions(duplicates.keys) + } + } + + /** + * ๊ฐœ๋ณ„ ์ƒ์‚ฐ ๊ทœ์น™์˜ ๊ตฌ์กฐ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateProductionStructure(production: Production) { + if (production.right.size > MAX_PRODUCTION_LENGTH) { + throw ParserException.productionLengthExceedsLimit( + length = production.right.size, + maxLength = MAX_PRODUCTION_LENGTH + ) + } + + if (production.id < 0) { + throw ParserException.productionIdNegative(production.id) + } + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ์‹ฌ๋ณผ๋“ค์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateProductionSymbols( + production: Production, + terminals: Set, + nonTerminals: Set + ) { + val allSymbols = terminals + nonTerminals + + if (production.left !in nonTerminals) { + throw ParserException.productionLeftNotNonTerminal(production.left) + } + + production.right.forEach { symbol -> + if (symbol !in allSymbols) { + throw ParserException.unknownSymbolInProduction(symbol, production.id) + } + } + } + + /** + * ์˜์กด์„ฑ ๊ทธ๋ž˜ํ”„๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun buildDependencyGraph(productions: List): Map> { + val graph = mutableMapOf>() + + productions.forEach { production -> + val dependencies = graph.getOrPut(production.left) { mutableSetOf() } + production.right.forEach { symbol -> + if (symbol.isNonTerminal()) { + dependencies.add(symbol) + } + } + } + + return graph + } + + /** + * ์ขŒ์žฌ๊ท€๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasLeftRecursion( + symbol: TokenType, + graph: Map>, + visited: MutableSet + ): Boolean { + if (symbol in visited) return true + + visited.add(symbol) + + val dependencies = graph[symbol] ?: emptySet() + for (dep in dependencies) { + if (hasLeftRecursion(dep, graph, visited)) { + return true + } + } + + visited.remove(symbol) + return false + } + + /** + * ๋„๋‹ฌ ๊ฐ€๋Šฅํ•œ ์‹ฌ๋ณผ๋“ค์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + private fun findReachableSymbols( + productions: List, + startSymbol: TokenType + ): Set { + val reachable = mutableSetOf() + val queue = mutableListOf(startSymbol) + + while (queue.isNotEmpty()) { + val current = queue.removeAt(0) + if (current in reachable) continue + + reachable.add(current) + + productions.filter { it.left == current }.forEach { production -> + production.right.forEach { symbol -> + if (symbol.isNonTerminal() && symbol !in reachable) { + queue.add(symbol) + } + } + } + } + + return reachable + } + + /** + * ์ˆœํ™˜ ์ฐธ์กฐ๋ฅผ ๊ฒ€์‚ฌํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasCycle( + current: TokenType, + graph: Map>, + visited: MutableSet, + recursionStack: MutableSet + ): Boolean { + visited.add(current) + recursionStack.add(current) + + val neighbors = graph[current] ?: emptySet() + for (neighbor in neighbors) { + if (neighbor !in visited) { + if (hasCycle(neighbor, graph, visited, recursionStack)) { + return true + } + } else if (neighbor in recursionStack) { + return true + } + } + + recursionStack.remove(current) + return false + } + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxProductionCount" to MAX_PRODUCTION_COUNT, + "maxProductionLength" to MAX_PRODUCTION_LENGTH, + "maxRecursionDepth" to MAX_RECURSION_DEPTH, + "minProductionCount" to MIN_PRODUCTION_COUNT, + "supportedValidations" to listOf( + "basicStructure", + "productionRules", + "reachability", + "completeness", + "leftRecursion", + "ambiguity", + "cycles" + ) + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/policies/LALRMergingPolicy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/policies/LALRMergingPolicy.kt new file mode 100644 index 00000000..287c5565 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/policies/LALRMergingPolicy.kt @@ -0,0 +1,407 @@ +package hs.kr.entrydsm.domain.parser.policies + +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.domain.parser.entities.ParsingState +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * LALR ์ƒํƒœ ๋ณ‘ํ•ฉ ์ •์ฑ…์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Policy ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ LALR(1) ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ตฌ์ถ• ์‹œ + * ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ง„ LR(1) ์ƒํƒœ๋“ค์„ ์•ˆ์ „ํ•˜๊ฒŒ ๋ณ‘ํ•ฉํ•˜๋Š” + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ ์••์ถ•์„ ํ†ตํ•ด ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” + * ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•˜๋ฉด์„œ๋„ ํŒŒ์‹ฑ ์ •ํ™•์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Policy( + name = "LALRMerging", + description = "LALR ์ƒํƒœ ๋ณ‘ํ•ฉ๊ณผ ์••์ถ•์„ ์œ„ํ•œ ์ •์ฑ…์œผ๋กœ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ์ตœ์ ํ™”๋ฅผ ๋‹ด๋‹น", + domain = "parser", + scope = Scope.DOMAIN +) +class LALRMergingPolicy { + + companion object { + private const val MAX_MERGING_ATTEMPTS = 1000 + private const val MAX_CORE_SIGNATURE_LENGTH = 512 + private const val MIN_LOOKAHEAD_OVERLAP = 0.0 + } + + private val mergingHistory = mutableListOf() + private var strictMerging = true + private var allowConflictMerging = false + + /** + * ๋‘ ํŒŒ์‹ฑ ์ƒํƒœ๊ฐ€ LALR ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param state1 ์ฒซ ๋ฒˆ์งธ ํŒŒ์‹ฑ ์ƒํƒœ + * @param state2 ๋‘ ๋ฒˆ์งธ ํŒŒ์‹ฑ ์ƒํƒœ + * @return ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun canMergeLALRStates(state1: ParsingState, state2: ParsingState): Boolean { + // 1. ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ ธ์•ผ ํ•จ + if (!haveSameCore(state1, state2)) { + return false + } + + // 2. ๋ณ‘ํ•ฉ ํ›„ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์•„์•ผ ํ•จ + if (strictMerging && !canMergeWithoutConflicts(state1, state2)) { + return false + } + + // 3. ์ „์ด ์ •๋ณด๊ฐ€ ํ˜ธํ™˜๋˜์–ด์•ผ ํ•จ + if (!areTransitionsCompatible(state1, state2)) { + return false + } + + return true + } + + /** + * ๋‘ LALR ์ƒํƒœ๋ฅผ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param state1 ์ฒซ ๋ฒˆ์งธ ํŒŒ์‹ฑ ์ƒํƒœ + * @param state2 ๋‘ ๋ฒˆ์งธ ํŒŒ์‹ฑ ์ƒํƒœ + * @return ๋ณ‘ํ•ฉ๋œ ์ƒˆ๋กœ์šด ํŒŒ์‹ฑ ์ƒํƒœ + * @throws IllegalArgumentException ๋ณ‘ํ•ฉ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋“ค์ธ ๊ฒฝ์šฐ + */ + fun mergeLALRStates(state1: ParsingState, state2: ParsingState): ParsingState { + if (!canMergeLALRStates(state1, state2)) { + throw ParserException.lalrStatesCannotMerge(state1.id, state2.id) + } + + val mergedItems = mergeItems(state1.items, state2.items) + val mergedTransitions = mergeTransitions(state1.transitions, state2.transitions) + val mergedActions = mergeActions(state1.actions, state2.actions) + val mergedGotos = mergeGotos(state1.gotos, state2.gotos) + + val mergedState = ParsingState( + id = state1.id, // ๋” ์ž‘์€ ID ์‚ฌ์šฉ + items = mergedItems, + transitions = mergedTransitions, + actions = mergedActions, + gotos = mergedGotos, + isAccepting = state1.isAccepting || state2.isAccepting, + isFinal = state1.isFinal || state2.isFinal, + metadata = state1.metadata + state2.metadata + ("mergedFrom" to listOf(state1.id, state2.id)) + ) + + recordMerging(state1, state2, mergedState, "Successful LALR merge") + return mergedState + } + + /** + * ํŒŒ์‹ฑ ์ƒํƒœ๋“ค์„ LALR ๋ฐฉ์‹์œผ๋กœ ์••์ถ•ํ•ฉ๋‹ˆ๋‹ค. + * + * @param states ์••์ถ•ํ•  ํŒŒ์‹ฑ ์ƒํƒœ๋“ค + * @return ์••์ถ•๋œ ํŒŒ์‹ฑ ์ƒํƒœ๋“ค + */ + fun compressStatesLALR(states: Map): Map { + val compressedStates = mutableMapOf() + val coreGroups = groupStatesByCore(states.values) + var newStateId = 0 + + coreGroups.forEach { (coreSignature, statesWithSameCore) -> + if (statesWithSameCore.size == 1) { + // ๋‹จ์ผ ์ƒํƒœ๋Š” ๊ทธ๋Œ€๋กœ ์œ ์ง€ + compressedStates[newStateId] = statesWithSameCore.first().copy(id = newStateId) + } else { + // ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ง„ ์ƒํƒœ๋“ค์„ ๋ณ‘ํ•ฉ + val mergedState = mergeMultipleStates(statesWithSameCore) + compressedStates[newStateId] = mergedState.copy(id = newStateId) + } + newStateId++ + } + + return compressedStates + } + + /** + * ์••์ถ•๋œ ์ƒํƒœ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ํŒŒ์‹ฑ ์ƒํƒœ + * @return ์••์ถ•๋œ ์‹œ๊ทธ๋‹ˆ์ฒ˜ ๋ฌธ์ž์—ด + */ + fun generateCoreSignature(state: ParsingState): String { + val coreItems = state.getKernelItems() + val signature = coreItems.sortedBy { "${it.production.id}:${it.dotPos}" } + .joinToString("|") { "${it.production.id}:${it.dotPos}" } + + return if (signature.length > MAX_CORE_SIGNATURE_LENGTH) { + signature.take(MAX_CORE_SIGNATURE_LENGTH) + "..." + } else { + signature + } + } + + /** + * LALR ๋ณ‘ํ•ฉ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param originalStates ์›๋ณธ ์ƒํƒœ๋“ค + * @param compressedStates ์••์ถ•๋œ ์ƒํƒœ๋“ค + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateLALRMerging( + originalStates: Map, + compressedStates: Map + ): Boolean { + // 1. ์••์ถ•๋ฅ ์ด ์ ์ ˆํ•œ์ง€ ํ™•์ธ + val compressionRatio = compressedStates.size.toDouble() / originalStates.size + if (compressionRatio > 0.9) { + // ์••์ถ• ํšจ๊ณผ๊ฐ€ ๋„ˆ๋ฌด ์ ์Œ + return false + } + + // 2. ๋ชจ๋“  ์ปค๋„ ์•„์ดํ…œ์ด ๋ณด์กด๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + val originalCores = originalStates.values.flatMap { it.getKernelItems() }.toSet() + val compressedCores = compressedStates.values.flatMap { it.getKernelItems() }.toSet() + + if (originalCores != compressedCores) { + return false + } + + // 3. ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์•˜๋Š”์ง€ ํ™•์ธ + val hasConflicts = compressedStates.values.any { state -> + state.getConflicts().isNotEmpty() + } + + return !hasConflicts || allowConflictMerging + } + + /** + * ๋ณ‘ํ•ฉ ๊ธฐ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณ‘ํ•ฉ ๊ธฐ๋ก ๋ชฉ๋ก + */ + fun getMergingHistory(): List = mergingHistory.toList() + + /** + * ์—„๊ฒฉํ•œ ๋ณ‘ํ•ฉ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param strict ์—„๊ฒฉํ•œ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + fun setStrictMerging(strict: Boolean) { + this.strictMerging = strict + } + + /** + * ์ถฉ๋Œ์ด ์žˆ๋Š” ๋ณ‘ํ•ฉ์„ ํ—ˆ์šฉํ• ์ง€ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param allow ์ถฉ๋Œ ๋ณ‘ํ•ฉ ํ—ˆ์šฉ ์—ฌ๋ถ€ + */ + fun setAllowConflictMerging(allow: Boolean) { + this.allowConflictMerging = allow + } + + // Private helper methods + + /** + * ๋‘ ์ƒํƒœ๊ฐ€ ๋™์ผํ•œ core๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun haveSameCore(state1: ParsingState, state2: ParsingState): Boolean { + val core1 = state1.getKernelItems() + val core2 = state2.getKernelItems() + + if (core1.size != core2.size) return false + + // ์ปค๋„ ์•„์ดํ…œ๋“ค์˜ production๊ณผ dotPos์ด ๋™์ผํ•ด์•ผ ํ•จ + val coreSet1 = core1.map { "${it.production.id}:${it.dotPos}" }.toSet() + val coreSet2 = core2.map { "${it.production.id}:${it.dotPos}" }.toSet() + + return coreSet1 == coreSet2 + } + + /** + * ์ถฉ๋Œ ์—†์ด ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun canMergeWithoutConflicts(state1: ParsingState, state2: ParsingState): Boolean { + // ๋ณ‘ํ•ฉ๋œ ์•ก์…˜์—์„œ ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•˜๋Š”์ง€ ํ™•์ธ + val allTerminals = (state1.actions.keys + state2.actions.keys).toSet() + + for (terminal in allTerminals) { + val action1 = state1.actions[terminal] + val action2 = state2.actions[terminal] + + if (action1 != null && action2 != null && action1 != action2) { + // ๋™์ผํ•œ ํ„ฐ๋ฏธ๋„์— ๋Œ€ํ•ด ๋‹ค๋ฅธ ์•ก์…˜์ด ์žˆ์œผ๋ฉด ์ถฉ๋Œ + return false + } + } + + return true + } + + /** + * ์ „์ด ์ •๋ณด๊ฐ€ ํ˜ธํ™˜๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun areTransitionsCompatible(state1: ParsingState, state2: ParsingState): Boolean { + val allSymbols = (state1.transitions.keys + state2.transitions.keys).toSet() + + for (symbol in allSymbols) { + val target1 = state1.transitions[symbol] + val target2 = state2.transitions[symbol] + + if (target1 != null && target2 != null && target1 != target2) { + // ๋™์ผํ•œ ์‹ฌ๋ณผ์— ๋Œ€ํ•ด ๋‹ค๋ฅธ ๋ชฉํ‘œ ์ƒํƒœ๊ฐ€ ์žˆ์œผ๋ฉด ํ˜ธํ™˜ ๋ถˆ๊ฐ€ + return false + } + } + + return true + } + + /** + * ์•„์ดํ…œ๋“ค์„ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ + private fun mergeItems(items1: Set, items2: Set): Set { + val mergedItems = mutableSetOf() + val itemGroups = (items1 + items2).groupBy { "${it.production.id}:${it.dotPos}" } + + itemGroups.values.forEach { group -> + if (group.size == 1) { + mergedItems.add(group.first()) + } else { + // ๊ฐ™์€ production๊ณผ dotPos์„ ๊ฐ€์ง„ ์•„์ดํ…œ๋“ค์˜ lookahead ๋ณ‘ํ•ฉ + val mergedLookaheads = group.map { it.lookahead }.toSet() + val mergedItem = group.first().copy(lookahead = mergedLookaheads.first()) + mergedItems.add(mergedItem) + } + } + + return mergedItems + } + + /** + * ์ „์ด ์ •๋ณด๋ฅผ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ + private fun mergeTransitions( + transitions1: Map, + transitions2: Map + ): Map { + val merged = transitions1.toMutableMap() + transitions2.forEach { (symbol, target) -> + merged[symbol] = target + } + return merged + } + + /** + * ์•ก์…˜ ์ •๋ณด๋ฅผ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ + private fun mergeActions( + actions1: Map, + actions2: Map + ): Map { + val merged = actions1.toMutableMap() + actions2.forEach { (terminal, action) -> + merged[terminal] = action + } + return merged + } + + /** + * Goto ์ •๋ณด๋ฅผ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ + private fun mergeGotos( + gotos1: Map, + gotos2: Map + ): Map { + val merged = gotos1.toMutableMap() + gotos2.forEach { (nonTerminal, target) -> + merged[nonTerminal] = target + } + return merged + } + + /** + * Core์— ๋”ฐ๋ผ ์ƒํƒœ๋“ค์„ ๊ทธ๋ฃนํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun groupStatesByCore(states: Collection): Map> { + return states.groupBy { generateCoreSignature(it) } + } + + /** + * ์—ฌ๋Ÿฌ ์ƒํƒœ๋ฅผ ๋ณ‘ํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ + private fun mergeMultipleStates(states: List): ParsingState { + if (states.isEmpty()) { + throw ParserException.noStatesToMerge() + } + + if (states.size == 1) { + return states.first() + } + + var result = states.first() + for (i in 1 until states.size) { + result = mergeLALRStates(result, states[i]) + } + + return result + } + + /** + * ๋ณ‘ํ•ฉ ๊ธฐ๋ก์„ ๋‚จ๊น๋‹ˆ๋‹ค. + */ + private fun recordMerging( + state1: ParsingState, + state2: ParsingState, + mergedState: ParsingState, + reason: String + ) { + mergingHistory.add( + MergingRecord( + sourceStates = listOf(state1.id, state2.id), + targetState = mergedState.id, + coreSignature = generateCoreSignature(mergedState), + reason = reason, + timestamp = System.currentTimeMillis() + ) + ) + } + + /** + * ๋ณ‘ํ•ฉ ๊ธฐ๋ก์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class MergingRecord( + val sourceStates: List, + val targetState: Int, + val coreSignature: String, + val reason: String, + val timestamp: Long + ) + + /** + * ์ •์ฑ…์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxMergingAttempts" to MAX_MERGING_ATTEMPTS, + "maxCoreSignatureLength" to MAX_CORE_SIGNATURE_LENGTH, + "minLookaheadOverlap" to MIN_LOOKAHEAD_OVERLAP, + "strictMerging" to strictMerging, + "allowConflictMerging" to allowConflictMerging, + "mergingStrategies" to listOf("coreEquivalence", "transitionCompatibility", "conflictAvoidance") + ) + + /** + * ์ •์ฑ…์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "policyName" to "LALRMergingPolicy", + "totalMergings" to mergingHistory.size, + "successfulMergings" to mergingHistory.count { it.reason.contains("Successful") }, + "averageCoreSignatureLength" to if (mergingHistory.isNotEmpty()) { + mergingHistory.map { it.coreSignature.length }.average() + } else 0.0 + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/ConflictResolver.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/ConflictResolver.kt new file mode 100644 index 00000000..15e1fd49 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/ConflictResolver.kt @@ -0,0 +1,358 @@ +package hs.kr.entrydsm.domain.parser.services + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.parser.values.OperatorPrecedence +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType + +/** + * LR ํŒŒ์‹ฑ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * Shift/Reduce์™€ Reduce/Reduce ์ถฉ๋Œ์„ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„์™€ ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์„ + * ๊ธฐ๋ฐ˜์œผ๋กœ ํ•ด๊ฒฐํ•˜์—ฌ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ์™„์„ฑํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ ์ถฉ๋Œ ํ•ด๊ฒฐ ๋กœ์ง์„ DDD ๊ตฌ์กฐ๋กœ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Service( + name = "ConflictResolver", + type = ServiceType.DOMAIN_SERVICE +) +class ConflictResolver { + + /** + * ๋‘ ์•ก์…˜ ๊ฐ„์˜ ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param existing ๊ธฐ์กด ์•ก์…˜ + * @param newAction ์ƒˆ๋กœ์šด ์•ก์…˜ + * @param lookahead ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ํ† ํฐ + * @param stateId ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์ƒํƒœ ID + * @return ํ•ด๊ฒฐ๋œ ์•ก์…˜ ๋˜๋Š” null (ํ•ด๊ฒฐ ๋ถˆ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ) + */ + fun resolveConflict( + existing: LRAction, + newAction: LRAction, + lookahead: TokenType, + stateId: Int + ): ConflictResolutionResult { + return when { + isShiftReduceConflict(existing, newAction) -> { + resolveShiftReduceConflict(existing as LRAction.Shift, newAction as LRAction.Reduce, lookahead, stateId) + } + isShiftReduceConflict(newAction, existing) -> { + resolveShiftReduceConflict(newAction as LRAction.Shift, existing as LRAction.Reduce, lookahead, stateId) + } + isReduceReduceConflict(existing, newAction) -> { + resolveReduceReduceConflict(existing as LRAction.Reduce, newAction as LRAction.Reduce, stateId) + } + areIdenticalActions(existing, newAction) -> { + ConflictResolutionResult.Resolved(existing, ConflictResolverConsts.MSG_IDENTICAL_ACTION) + } + else -> { + ConflictResolutionResult.Unresolved( + ConflictResolverConsts.MSG_UNSUPPORTED.format(existing, newAction) + ) + } + } + } + + /** + * Shift/Reduce ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„์™€ ๊ฒฐํ•ฉ์„ฑ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun resolveShiftReduceConflict( + shiftAction: LRAction.Shift, + reduceAction: LRAction.Reduce, + lookahead: TokenType, + stateId: Int + ): ConflictResolutionResult { + val lookaheadPrec = OperatorPrecedence.getPrecedence(lookahead) + val productionPrec = getProductionPrecedence(reduceAction.production) + + if (lookaheadPrec == null || productionPrec == null) { + // ์šฐ์„ ์ˆœ์œ„ ์ •๋ณด๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ์ ์œผ๋กœ Shift ์„ ํƒ (LR ํŒŒ์„œ์˜ ๊ธฐ๋ณธ ๋™์ž‘) + return ConflictResolutionResult.Resolved( + shiftAction, + ConflictResolverConsts.SR_DEFAULT_SHIFT + ) + } + + return when { + lookaheadPrec.hasHigherPrecedenceThan(productionPrec) -> { + ConflictResolutionResult.Resolved( + shiftAction, + ConflictResolverConsts.SR_LOOKAHEAD_HIGHER.format(lookaheadPrec.precedence, productionPrec.precedence) + ) + } + productionPrec.hasHigherPrecedenceThan(lookaheadPrec) -> { + ConflictResolutionResult.Resolved( + reduceAction, + ConflictResolverConsts.SR_PRODUCTION_HIGHER.format(productionPrec.precedence, lookaheadPrec.precedence) + ) + } + lookaheadPrec.hasSamePrecedenceAs(productionPrec) -> { + when { + lookaheadPrec.isLeftAssociative() -> { + ConflictResolutionResult.Resolved( + reduceAction, + ConflictResolverConsts.SR_LEFT_ASSOC_REDUCE + ) + } + lookaheadPrec.isRightAssociative() -> { + ConflictResolutionResult.Resolved( + shiftAction, + ConflictResolverConsts.SR_RIGHT_ASSOC_SHIFT + ) + } + lookaheadPrec.isNonAssociative() -> { + ConflictResolutionResult.Unresolved( + ConflictResolverConsts.SR_NON_ASSOC + ) + } + else -> { + ConflictResolutionResult.Unresolved( + ConflictResolverConsts.SR_UNKNOWN_ASSOC.format(lookaheadPrec.associativity) + ) + } + } + } + else -> { + ConflictResolutionResult.Unresolved(ConflictResolverConsts.SR_COMPARE_FAIL) + } + } + } + + /** + * Reduce/Reduce ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * ์ผ๋ฐ˜์ ์œผ๋กœ ๋” ๊ธด ์ƒ์‚ฐ ๊ทœ์น™์„ ์„ ํƒํ•˜๊ฑฐ๋‚˜, ๋ฌธ๋ฒ•์—์„œ ๋จผ์ € ์ •์˜๋œ ๊ฒƒ์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. + */ + private fun resolveReduceReduceConflict( + existingReduce: LRAction.Reduce, + newReduce: LRAction.Reduce, + stateId: Int + ): ConflictResolutionResult { + val existing = existingReduce.production + val new = newReduce.production + + return when { + existing.length > new.length -> { + ConflictResolutionResult.Resolved( + existingReduce, + ConflictResolverConsts.RR_EXISTING_LONGER.format(existing.length, new.length) + ) + } + new.length > existing.length -> { + ConflictResolutionResult.Resolved( + newReduce, + ConflictResolverConsts.RR_NEW_LONGER.format(new.length, existing.length) + ) + } + existing.id < new.id -> { + ConflictResolutionResult.Resolved( + existingReduce, + ConflictResolverConsts.RR_EXISTING_FIRST.format(existing.id, new.id) + ) + } + new.id < existing.id -> { + ConflictResolutionResult.Resolved( + newReduce, + ConflictResolverConsts.RR_NEW_FIRST.format(new.id, existing.id) + ) + } + else -> { + // ๊ธธ์ด์™€ ID๊ฐ€ ๋ชจ๋‘ ๊ฐ™์€ ๊ฒฝ์šฐ - ์ด๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ ๋ฐœ์ƒํ•˜์ง€ ์•Š์•„์•ผ ํ•จ + ConflictResolutionResult.Resolved( + existingReduce, + ConflictResolverConsts.RR_SAME_RULE + ) + } + } + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ๊ฐ€์žฅ ์˜ค๋ฅธ์ชฝ ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + */ + private fun getProductionPrecedence(production: Production): OperatorPrecedence? { + for (i in production.right.indices.reversed()) { + val symbol = production.right[i] + val precedence = OperatorPrecedence.getPrecedence(symbol) + if (precedence != null) return precedence + } + return null + } + + /** + * ์ถฉ๋Œ ํ†ต๊ณ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param conflicts ์ถฉ๋Œ ๋ชฉ๋ก + * @return ์ถฉ๋Œ ํ†ต๊ณ„ ๋งต + */ + fun generateConflictStatistics(conflicts: List): Map { + val shiftReduceCount = conflicts.count { it.type == ConflictType.SHIFT_REDUCE } + val reduceReduceCount = conflicts.count { it.type == ConflictType.REDUCE_REDUCE } + val resolvedCount = conflicts.count { it.resolved } + val unresolvedCount = conflicts.size - resolvedCount + + return mapOf( + ConflictResolverConsts.KEY_TOTAL to conflicts.size, + ConflictResolverConsts.KEY_SR to shiftReduceCount, + ConflictResolverConsts.KEY_RR to reduceReduceCount, + ConflictResolverConsts.KEY_RESOLVED to resolvedCount, + ConflictResolverConsts.KEY_UNRESOLVED to unresolvedCount, + ConflictResolverConsts.KEY_RATE to if (conflicts.isNotEmpty()) { + resolvedCount.toDouble() / conflicts.size + } else 1.0, + ConflictResolverConsts.KEY_BY_STATE to conflicts + .groupBy { it.stateId } + .mapValues { it.value.size } + ) + } + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param conflicts ์ถฉ๋Œ ๋ชฉ๋ก + * @return ์ถฉ๋Œ ํ•ด๊ฒฐ ๋ณด๊ณ ์„œ + */ + fun generateConflictReport(conflicts: List): String { + val stats = generateConflictStatistics(conflicts) + val sb = StringBuilder() + + sb.appendLine(ConflictResolverConsts.REPORT_TITLE) + sb.appendLine(ConflictResolverConsts.REPORT_TOTAL + stats[ConflictResolverConsts.KEY_TOTAL]) + sb.appendLine(ConflictResolverConsts.REPORT_SR + stats[ConflictResolverConsts.KEY_SR]) + sb.appendLine(ConflictResolverConsts.REPORT_RR + stats[ConflictResolverConsts.KEY_RR]) + sb.appendLine(ConflictResolverConsts.REPORT_RESOLVED + stats[ConflictResolverConsts.KEY_RESOLVED]) + sb.appendLine(ConflictResolverConsts.REPORT_UNRESOLVED + stats[ConflictResolverConsts.KEY_UNRESOLVED]) + sb.appendLine( + ConflictResolverConsts.REPORT_RATE + + String.format("%.2f%%", (stats[ConflictResolverConsts.KEY_RATE] as Double) * 100) + ) + sb.appendLine() + + if (conflicts.any { !it.resolved }) { + sb.appendLine(ConflictResolverConsts.REPORT_UNRESOLVED_HEADER) + conflicts.filter { !it.resolved }.forEach { conflict -> + sb.appendLine("${ConflictResolverConsts.STATE_PREFIX}${conflict.stateId}: ${conflict.description}") + } + sb.appendLine() + } + + if (conflicts.any { it.resolved }) { + sb.appendLine(ConflictResolverConsts.REPORT_RESOLVED_HEADER) + conflicts.filter { it.resolved }.take(5).forEach { conflict -> + sb.appendLine("${ConflictResolverConsts.STATE_PREFIX}${conflict.stateId}: ${conflict.description} -> ${conflict.resolution}") + } + } + + return sb.toString() + } + + /** + * Shift/Reduce ์ถฉ๋Œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isShiftReduceConflict(a: LRAction, b: LRAction): Boolean = + a is LRAction.Shift && b is LRAction.Reduce + + /** + * Reduce/Reduce ์ถฉ๋Œ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isReduceReduceConflict(a: LRAction, b: LRAction): Boolean = + a is LRAction.Reduce && b is LRAction.Reduce + + /** + * ๋‘ ์•ก์…˜์ด ๋™์ผํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun areIdenticalActions(a: LRAction, b: LRAction): Boolean = a == b + + companion object { + /** + * ์‹ฑ๊ธ€ํ†ค ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun create(): ConflictResolver = ConflictResolver() + } +} + +/** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” sealed class์ž…๋‹ˆ๋‹ค. + * + * ํƒ€์ž…์„ ๋ช…ํ™•ํžˆ ๊ตฌ๋ถ„ํ•˜์—ฌ ๋ชจ์ˆœ๋˜๋Š” ์ƒํƒœ๋ฅผ ๋ฐฉ์ง€ํ•˜๊ณ , + * when ์‹์—์„œ smart cast๊ฐ€ ๊ฐ€๋Šฅํ•˜์—ฌ ์ฝ”๋“œ ์•ˆ์ „์„ฑ๊ณผ ๊ฐ€๋…์„ฑ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + */ +sealed class ConflictResolutionResult { + data class Resolved(val action: LRAction, val reason: String) : ConflictResolutionResult() + data class Unresolved(val reason: String) : ConflictResolutionResult() +} + +/** + * ์ถฉ๋Œ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class ConflictInfo( + val stateId: Int, + val type: ConflictType, + val description: String, + val resolved: Boolean, + val resolution: String? = null +) + +/** + * ์ถฉ๋Œ ํƒ€์ž…์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ +enum class ConflictType { + SHIFT_REDUCE, + REDUCE_REDUCE, + ACCEPT_REDUCE +} + +/** + * ConflictResolver์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ƒ์ˆ˜ ๋ชจ์Œ + */ +object ConflictResolverConsts { + // ๊ณตํ†ต ๋ฉ”์‹œ์ง€/์ ‘๋‘์–ด + const val MSG_IDENTICAL_ACTION = "๋™์ผํ•œ ์•ก์…˜" + const val MSG_UNSUPPORTED = "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์ถฉ๋Œ ์œ ํ˜•: %s vs %s" + const val STATE_PREFIX = "์ƒํƒœ " + + // Shift/Reduce ํ•ด๊ฒฐ ๋ฉ”์‹œ์ง€ + const val SR_DEFAULT_SHIFT = "์šฐ์„ ์ˆœ์œ„ ์ •๋ณด ์—†์Œ, Shift ์„ ํƒ (๊ธฐ๋ณธ ๊ทœ์น™)" + const val SR_LOOKAHEAD_HIGHER = "Lookahead ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์Œ (%d > %d)" + const val SR_PRODUCTION_HIGHER = "Production ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋†’์Œ (%d > %d)" + const val SR_LEFT_ASSOC_REDUCE = "์ขŒ๊ฒฐํ•ฉ, Reduce ์„ ํƒ" + const val SR_RIGHT_ASSOC_SHIFT = "์šฐ๊ฒฐํ•ฉ, Shift ์„ ํƒ" + const val SR_NON_ASSOC = "๋น„๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž ์ถฉ๋Œ, ํ•ด๊ฒฐ ๋ถˆ๊ฐ€๋Šฅ" + const val SR_UNKNOWN_ASSOC = "์•Œ ์ˆ˜ ์—†๋Š” ๊ฒฐํ•ฉ์„ฑ: %s" + const val SR_COMPARE_FAIL = "์šฐ์„ ์ˆœ์œ„ ๋น„๊ต ์‹คํŒจ" + + // Reduce/Reduce ํ•ด๊ฒฐ ๋ฉ”์‹œ์ง€ + const val RR_EXISTING_LONGER = "๊ธฐ์กด ์ƒ์‚ฐ ๊ทœ์น™์ด ๋” ๊น€ (%d > %d)" + const val RR_NEW_LONGER = "์ƒˆ ์ƒ์‚ฐ ๊ทœ์น™์ด ๋” ๊น€ (%d > %d)" + const val RR_EXISTING_FIRST = "๊ธฐ์กด ์ƒ์‚ฐ ๊ทœ์น™์ด ๋จผ์ € ์ •์˜๋จ (ID: %d < %d)" + const val RR_NEW_FIRST = "์ƒˆ ์ƒ์‚ฐ ๊ทœ์น™์ด ๋จผ์ € ์ •์˜๋จ (ID: %d < %d)" + const val RR_SAME_RULE = "๋™์ผํ•œ ์ƒ์‚ฐ ๊ทœ์น™, ๊ธฐ์กด ์„ ํƒ" + + // ํ†ต๊ณ„ ํ‚ค + const val KEY_TOTAL = "totalConflicts" + const val KEY_SR = "shiftReduceConflicts" + const val KEY_RR = "reduceReduceConflicts" + const val KEY_RESOLVED = "resolvedConflicts" + const val KEY_UNRESOLVED = "unresolvedConflicts" + const val KEY_RATE = "resolutionRate" + const val KEY_BY_STATE = "conflictsByState" + + // ๋ฆฌํฌํŠธ ํ…์ŠคํŠธ + const val REPORT_TITLE = "=== LR ํŒŒ์‹ฑ ์ถฉ๋Œ ํ•ด๊ฒฐ ๋ณด๊ณ ์„œ ===" + const val REPORT_TOTAL = "์ด ์ถฉ๋Œ ์ˆ˜: " + const val REPORT_SR = "Shift/Reduce ์ถฉ๋Œ: " + const val REPORT_RR = "Reduce/Reduce ์ถฉ๋Œ: " + const val REPORT_RESOLVED = "ํ•ด๊ฒฐ๋œ ์ถฉ๋Œ: " + const val REPORT_UNRESOLVED = "๋ฏธํ•ด๊ฒฐ ์ถฉ๋Œ: " + const val REPORT_RATE = "ํ•ด๊ฒฐ๋ฅ : " + const val REPORT_UNRESOLVED_HEADER = "=== ๋ฏธํ•ด๊ฒฐ ์ถฉ๋Œ ๋ชฉ๋ก ===" + const val REPORT_RESOLVED_HEADER = "=== ํ•ด๊ฒฐ๋œ ์ถฉ๋Œ ์ƒ˜ํ”Œ ===" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/ConflictResolverService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/ConflictResolverService.kt new file mode 100644 index 00000000..223b1337 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/ConflictResolverService.kt @@ -0,0 +1,497 @@ +package hs.kr.entrydsm.domain.parser.services + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.ParsingState +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.values.Associativity +import hs.kr.entrydsm.domain.parser.values.Grammar +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.parser.values.ParsingTable +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider + +/** + * ํŒŒ์‹ฑ ์ถฉ๋Œ ํ•ด๊ฒฐ์„ ๋‹ด๋‹นํ•˜๋Š” ๋„๋ฉ”์ธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Domain Service ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ LR ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š” + * Shift/Reduce ๋ฐ Reduce/Reduce ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•˜๋Š” ๋ณต์žกํ•œ ๋กœ์ง์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„์™€ ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์„ ํ™œ์šฉํ•˜์—ฌ ์ถฉ๋Œ์„ ์ฒด๊ณ„์ ์œผ๋กœ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Service( + name = "ConflictResolverService", + type = ServiceType.DOMAIN_SERVICE +) +class ConflictResolverService( + private val configurationProvider: ConfigurationProvider +) { + + companion object { + private const val MAX_RESOLUTION_ATTEMPTS = 1000 + } + + private val associativityRules = Associativity.getDefaultRuleMap().toMutableMap() + private var resolutionStrategy = ResolutionStrategy.PRECEDENCE_BASED + private val resolutionHistory = mutableListOf() + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ์ „๋žต์„ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class ResolutionStrategy(val description: String) { + PRECEDENCE_BASED("์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜ ํ•ด๊ฒฐ"), + ASSOCIATIVITY_BASED("๊ฒฐํ•ฉ์„ฑ ๊ธฐ๋ฐ˜ ํ•ด๊ฒฐ"), + HYBRID("์šฐ์„ ์ˆœ์œ„์™€ ๊ฒฐํ•ฉ์„ฑ ๊ฒฐํ•ฉ"), + MANUAL("์ˆ˜๋™ ํ•ด๊ฒฐ"), + ERROR_ON_CONFLICT("์ถฉ๋Œ ์‹œ ์—๋Ÿฌ ๋ฐœ์ƒ") + } + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ๊ธฐ๋ก์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class ResolutionRecord( + val stateId: Int, + val conflictType: String, + val conflictSymbol: TokenType, + val resolution: String, + val timestamp: Long = System.currentTimeMillis() + ) + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ๋ชจ๋“  ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param parsingTable ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•  ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” + * @return ์ถฉ๋Œ์ด ํ•ด๊ฒฐ๋œ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” + */ + fun resolveConflicts(parsingTable: ParsingTable): ParsingTable { + var resolvedTable = parsingTable + var attemptCount = 0 + + while (attemptCount < MAX_RESOLUTION_ATTEMPTS) { + val conflicts = resolvedTable.getConflicts() + + if (conflicts.isEmpty()) { + break // ๋ชจ๋“  ์ถฉ๋Œ์ด ํ•ด๊ฒฐ๋จ + } + + resolvedTable = resolveTableConflicts(resolvedTable, conflicts) + attemptCount++ + } + + if (attemptCount >= MAX_RESOLUTION_ATTEMPTS) { + throw ParserException.conflictResolutionExceeded(attempts = attemptCount, maxAttempts = MAX_RESOLUTION_ATTEMPTS) + } + + return resolvedTable + } + + /** + * Shift/Reduce ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์ƒํƒœ + * @param shiftAction Shift ์•ก์…˜ + * @param reduceAction Reduce ์•ก์…˜ + * @param conflictSymbol ์ถฉ๋Œ ์‹ฌ๋ณผ + * @return ํ•ด๊ฒฐ๋œ ์•ก์…˜ + */ + fun resolveShiftReduceConflict( + state: ParsingState, + shiftAction: LRAction, + reduceAction: LRAction, + conflictSymbol: TokenType + ): LRAction { + val resolution = when (resolutionStrategy) { + ResolutionStrategy.PRECEDENCE_BASED -> + resolveByPrecedence(shiftAction, reduceAction, conflictSymbol) + + ResolutionStrategy.ASSOCIATIVITY_BASED -> + resolveByAssociativity(shiftAction, reduceAction, conflictSymbol) + + ResolutionStrategy.HYBRID -> + resolveByPrecedence(shiftAction, reduceAction, conflictSymbol) + + ResolutionStrategy.MANUAL -> + resolveManually(state, shiftAction, reduceAction, conflictSymbol) + + ResolutionStrategy.ERROR_ON_CONFLICT -> + throw ParserException.shiftReduceConflict(conflictSymbol = conflictSymbol, stateId = state.id) + } + + recordResolution( + state.id, + "shift_reduce", + conflictSymbol, + "Resolved to ${if (resolution.isShift()) "shift" else "reduce"}" + ) + + return resolution + } + + /** + * Reduce/Reduce ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ถฉ๋Œ์ด ๋ฐœ์ƒํ•œ ์ƒํƒœ + * @param reduceAction1 ์ฒซ ๋ฒˆ์งธ Reduce ์•ก์…˜ + * @param reduceAction2 ๋‘ ๋ฒˆ์งธ Reduce ์•ก์…˜ + * @param conflictSymbol ์ถฉ๋Œ ์‹ฌ๋ณผ + * @return ํ•ด๊ฒฐ๋œ ์•ก์…˜ + */ + fun resolveReduceReduceConflict( + state: ParsingState, + reduceAction1: LRAction, + reduceAction2: LRAction, + conflictSymbol: TokenType + ): LRAction { + val resolution = when (resolutionStrategy) { + ResolutionStrategy.PRECEDENCE_BASED -> + resolveReduceConflictByPrecedence(reduceAction1, reduceAction2) + + ResolutionStrategy.MANUAL -> + resolveReduceConflictManually(state, reduceAction1, reduceAction2, conflictSymbol) + + else -> + throw ParserException.reduceReduceConflictUnresolvable(conflictSymbol = conflictSymbol, stateId = state.id) + } + + recordResolution( + state.id, + "reduce_reduce", + conflictSymbol, + "Resolved to production ${resolution.getProductionId()}" + ) + + return resolution + } + + /** + * ๋ฌธ๋ฒ•์— ํ•ด๊ฒฐ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ถฉ๋Œ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param grammar ํ™•์ธํ•  ๋ฌธ๋ฒ• + * @return ํ•ด๊ฒฐ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ถฉ๋Œ์ด ์žˆ์œผ๋ฉด true + */ + fun hasUnresolvableConflicts(grammar: Grammar): Boolean { + return try { + val tempService = LRParserTableService( + lrItemFactory = hs.kr.entrydsm.domain.parser.factories.LRItemFactory(), + parsingStateFactory = hs.kr.entrydsm.domain.parser.factories.ParsingStateFactory(), + firstFollowCalculatorService = FirstFollowCalculatorService(), + configurationProvider = configurationProvider + ) + + val parsingTable = tempService.buildParsingTable(grammar) + val conflicts = parsingTable.getConflicts() + + if (conflicts.isEmpty()) { + false + } else { + resolveConflicts(parsingTable) + false + } + } catch (e: Exception) { + true + } + } + + /** + * ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๊ทœ์น™์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param associativity ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™ + */ + fun addAssociativityRule(associativity: Associativity) { + associativityRules[associativity.operator] = associativity + } + + /** + * ์—ฌ๋Ÿฌ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๊ทœ์น™์„ ํ•œ ๋ฒˆ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param associativities ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™๋“ค + */ + fun addAssociativityRules(associativities: List) { + associativities.forEach { associativity -> + associativityRules[associativity.operator] = associativity + } + } + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ์ „๋žต์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param strategy ์ƒˆ๋กœ์šด ํ•ด๊ฒฐ ์ „๋žต + */ + fun setResolutionStrategy(strategy: ResolutionStrategy) { + resolutionStrategy = strategy + } + + /** + * ํ˜„์žฌ ํ•ด๊ฒฐ ์ „๋žต์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ˜„์žฌ ํ•ด๊ฒฐ ์ „๋žต + */ + fun getResolutionStrategy(): ResolutionStrategy = resolutionStrategy + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ๊ธฐ๋ก์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•ด๊ฒฐ ๊ธฐ๋ก ๋ชฉ๋ก + */ + fun getResolutionHistory(): List = resolutionHistory.toList() + + /** + * ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun reset() { + resolutionHistory.clear() + resolutionStrategy = ResolutionStrategy.PRECEDENCE_BASED + associativityRules.clear() + associativityRules.putAll(Associativity.getDefaultRuleMap()) + } + + // Private helper methods + + private fun resolveTableConflicts( + parsingTable: ParsingTable, + conflicts: Map> + ): ParsingTable { + val newActionTable = parsingTable.actionTable.toMutableMap() + + parsingTable.states.values.forEach { state -> + val stateConflicts = state.getConflicts() + + stateConflicts.forEach { (conflictType, conflictDetails) -> + when (conflictType) { + "shift_reduce" -> resolveStateShiftReduceConflicts(state, newActionTable) + "reduce_reduce" -> resolveStateReduceReduceConflicts(state, newActionTable) + } + } + } + + return parsingTable.copy(actionTable = newActionTable) + } + + private fun resolveStateShiftReduceConflicts( + state: ParsingState, + actionTable: MutableMap, LRAction> + ) { + state.actions.forEach { (terminal, action) -> + if (action.isShift()) { + // ๋™์ผํ•œ ํ„ฐ๋ฏธ๋„์— ๋Œ€ํ•œ reduce ์•ก์…˜ ์ฐพ๊ธฐ + val reduceItems = state.items.filter { + it.isComplete() && terminal == it.lookahead + } + + if (reduceItems.isNotEmpty()) { + val reduceAction = LRAction.Reduce(reduceItems.first().production) + val resolvedAction = resolveShiftReduceConflict( + state, action, reduceAction, terminal + ) + actionTable[state.id to terminal] = resolvedAction + } + } + } + } + + private fun resolveStateReduceReduceConflicts( + state: ParsingState, + actionTable: MutableMap, LRAction> + ) { + val completeItems = state.items.filter { it.isComplete() } + val terminalGroups = completeItems.groupBy { item -> + item.lookahead + } + + terminalGroups.forEach { (terminal, items) -> + if (items.size > 1) { + val actions = items.map { LRAction.Reduce(it.production) } + if (actions.size > 1) { + val resolvedAction = resolveReduceReduceConflict( + state, actions[0], actions[1], terminal + ) + actionTable[state.id to terminal] = resolvedAction + } + } + } + } + + private fun resolveByPrecedence( + shiftAction: LRAction, + reduceAction: LRAction, + conflictSymbol: TokenType + ): LRAction { + // ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋จผ์ € ํ™•์ธํ•˜๊ณ , ๊ฐ™์œผ๋ฉด ๊ฒฐํ•ฉ์„ฑ์œผ๋กœ ํ•ด๊ฒฐ (PRECEDENCE_BASED, HYBRID ์ „๋žต ๊ณตํ†ต ๋กœ์ง) + val shiftPrecedence = getOperatorPrecedence(conflictSymbol) + val reducePrecedence = getReduceOperatorPrecedence(reduceAction) + + return when { + shiftPrecedence > reducePrecedence -> shiftAction + shiftPrecedence < reducePrecedence -> reduceAction + else -> resolveByAssociativity(shiftAction, reduceAction, conflictSymbol) + } + } + + private fun resolveByAssociativity( + shiftAction: LRAction, + reduceAction: LRAction, + conflictSymbol: TokenType + ): LRAction { + val associativity = associativityRules[conflictSymbol] + + return when (associativity?.type) { + Associativity.AssociativityType.LEFT -> reduceAction + Associativity.AssociativityType.RIGHT -> shiftAction + Associativity.AssociativityType.NONE -> + throw ParserException.nonAssociativeOperatorConflict(conflictSymbol = conflictSymbol) + else -> shiftAction // ๊ธฐ๋ณธ๊ฐ’ + } + } + + + private fun resolveManually( + state: ParsingState, + shiftAction: LRAction, + reduceAction: LRAction, + conflictSymbol: TokenType + ): LRAction { + return when { + conflictSymbol == TokenType.LEFT_PAREN -> shiftAction + conflictSymbol == TokenType.RIGHT_PAREN -> reduceAction + conflictSymbol == TokenType.COMMA && isInFunctionCall(state) -> shiftAction + conflictSymbol == TokenType.IF -> shiftAction + isArithmeticOperator(conflictSymbol) -> reduceAction + conflictSymbol == TokenType.AND -> shiftAction + conflictSymbol == TokenType.OR -> reduceAction + isComparisonOperator(conflictSymbol) -> reduceAction + else -> resolveByPrecedence(shiftAction, reduceAction, conflictSymbol) + } + } + + /** + * ํ˜„์žฌ ์ƒํƒœ๊ฐ€ ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋‚ด๋ถ€์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isInFunctionCall(state: ParsingState): Boolean { + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ํŒจํ„ด ๊ฐ์ง€ ๋กœ์ง + return state.items.any { item -> + item.production.right.contains(TokenType.LEFT_PAREN) && + item.production.right.contains(TokenType.RIGHT_PAREN) + } + } + + /** + * ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isArithmeticOperator(token: TokenType): Boolean { + return when (token) { + TokenType.PLUS, TokenType.MINUS, TokenType.MULTIPLY, TokenType.DIVIDE, + TokenType.POWER, TokenType.MODULO -> true + else -> false + } + } + + /** + * ๋น„๊ต ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isComparisonOperator(token: TokenType): Boolean { + return token.isComparisonOperator() + } + + private fun resolveReduceConflictByPrecedence( + reduceAction1: LRAction, + reduceAction2: LRAction + ): LRAction { + val precedence1 = getReduceOperatorPrecedence(reduceAction1) + val precedence2 = getReduceOperatorPrecedence(reduceAction2) + + return when { + precedence1 > precedence2 -> reduceAction1 + precedence1 < precedence2 -> reduceAction2 + else -> { + // ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๊ฐ™์œผ๋ฉด ๋” ๋‚ฎ์€ ID์˜ ์ƒ์‚ฐ ๊ทœ์น™ ์„ ํƒ + if (reduceAction1.getProductionId() < reduceAction2.getProductionId()) { + reduceAction1 + } else { + reduceAction2 + } + } + } + } + + private fun resolveReduceConflictManually( + state: ParsingState, + reduceAction1: LRAction, + reduceAction2: LRAction, + conflictSymbol: TokenType + ): LRAction { + // ์ˆ˜๋™ ํ•ด๊ฒฐ ๋กœ์ง (ํ˜„์žฌ๋Š” ์šฐ์„ ์ˆœ์œ„ ๊ธฐ๋ฐ˜์œผ๋กœ ํด๋ฐฑ) + return resolveReduceConflictByPrecedence(reduceAction1, reduceAction2) + } + + private fun getOperatorPrecedence(operator: TokenType): Int { + return associativityRules[operator]?.precedence ?: 0 + } + + private fun getReduceOperatorPrecedence(reduceAction: LRAction): Int { + // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„: production ID ๊ธฐ๋ฐ˜์œผ๋กœ ์šฐ์„ ์ˆœ์œ„ ์ถ”์ • + // ์‹ค์ œ๋กœ๋Š” ์ƒ์‚ฐ ๊ทœ์น™์˜ ์—ฐ์‚ฐ์ž๋ฅผ ๋ถ„์„ํ•ด์•ผ ํ•จ + return when (reduceAction.getProductionId()) { + in 0..10 -> 1 // ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„ (๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž) + in 11..20 -> 5 // ์ค‘๊ฐ„ ์šฐ์„ ์ˆœ์œ„ (์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž) + in 21..30 -> 8 // ๋†’์€ ์šฐ์„ ์ˆœ์œ„ (๋‹จํ•ญ ์—ฐ์‚ฐ์ž) + else -> 0 + } + } + + private fun recordResolution( + stateId: Int, + conflictType: String, + conflictSymbol: TokenType, + resolution: String + ) { + resolutionHistory.add( + ResolutionRecord(stateId, conflictType, conflictSymbol, resolution) + ) + } + + /** + * ์„œ๋น„์Šค์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxResolutionAttempts" to MAX_RESOLUTION_ATTEMPTS, + "currentStrategy" to resolutionStrategy.description, + "associativityRulesCount" to associativityRules.size, + "supportedConflictTypes" to listOf("shift_reduce", "reduce_reduce"), + "resolutionStrategies" to ResolutionStrategy.values().map { it.description } + ) + + /** + * ์„œ๋น„์Šค ์‚ฌ์šฉ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "serviceName" to "ConflictResolverService", + "totalResolutions" to resolutionHistory.size, + "resolutionsByType" to resolutionHistory.groupBy { it.conflictType } + .mapValues { it.value.size }, + "resolutionsByStrategy" to mapOf( + "currentStrategy" to resolutionStrategy.name, + "historySize" to resolutionHistory.size + ) + ) +} + +// Extension functions for LRAction type checking +fun hs.kr.entrydsm.domain.parser.values.LRAction.isShift(): Boolean = this is hs.kr.entrydsm.domain.parser.values.LRAction.Shift +fun hs.kr.entrydsm.domain.parser.values.LRAction.isReduce(): Boolean = this is hs.kr.entrydsm.domain.parser.values.LRAction.Reduce +fun hs.kr.entrydsm.domain.parser.values.LRAction.isAccept(): Boolean = this is hs.kr.entrydsm.domain.parser.values.LRAction.Accept +fun hs.kr.entrydsm.domain.parser.values.LRAction.isError(): Boolean = this is hs.kr.entrydsm.domain.parser.values.LRAction.Error +fun hs.kr.entrydsm.domain.parser.values.LRAction.getProductionId(): Int { + return if (this is hs.kr.entrydsm.domain.parser.values.LRAction.Reduce) { + this.production.id + } else { + throw ParserException.productionIdOnlyForReduce() + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/FirstFollowCalculatorService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/FirstFollowCalculatorService.kt new file mode 100644 index 00000000..399be4aa --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/FirstFollowCalculatorService.kt @@ -0,0 +1,475 @@ +package hs.kr.entrydsm.domain.parser.services + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType + +/** + * FIRST/FOLLOW ์ง‘ํ•ฉ ๊ณ„์‚ฐ์„ ๋‹ด๋‹นํ•˜๋Š” ๋„๋ฉ”์ธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Domain Service ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ LR ํŒŒ์‹ฑ์— ํ•„์š”ํ•œ FIRST์™€ FOLLOW ์ง‘ํ•ฉ์„ + * ๊ณ„์‚ฐํ•˜๋Š” ๋ณต์žกํ•œ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ตฌ์ถ• ๊ณผ์ •์—์„œ + * ์ „๋ฐฉํƒ์ƒ‰(lookahead) ๊ณ„์‚ฐ์— ํ•„์ˆ˜์ ์ธ ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Service( + name = "FirstFollowCalculatorService", + type = ServiceType.DOMAIN_SERVICE +) +class FirstFollowCalculatorService { + + companion object { + private const val MAX_ITERATIONS = 1000 + private const val CACHE_SIZE_LIMIT = 500 + + // Configuration keys + private const val KEY_MAX_ITERATIONS = "maxIterations" + private const val KEY_CACHE_SIZE_LIMIT = "cacheSizeLimit" + private const val KEY_ALGORITHMS = "algorithms" + private const val KEY_OPTIMIZATIONS = "optimizations" + + // Statistics keys + private const val KEY_SERVICE_NAME = "serviceName" + private const val KEY_CACHE_STATISTICS = "cacheStatistics" + private const val KEY_ALGORITHMS_IMPLEMENTED = "algorithmsImplemented" + + // Algorithm names + private const val ALGORITHM_FIRST_SET = "FirstSetCalculation" + private const val ALGORITHM_FOLLOW_SET = "FollowSetCalculation" + private const val ALGORITHM_SEQUENCE_FIRST = "SequenceFirstCalculation" + + // Optimization names + private const val OPTIMIZATION_CACHING = "caching" + private const val OPTIMIZATION_ITERATIVE_FIXPOINT = "iterativeFixpoint" + private const val OPTIMIZATION_EARLY_TERMINATION = "earlyTermination" + + // Service info + private const val SERVICE_NAME = "FirstFollowCalculatorService" + private const val ALGORITHMS_COUNT = 3 + + // Collections + private val ALGORITHMS = listOf( + ALGORITHM_FIRST_SET, + ALGORITHM_FOLLOW_SET, + ALGORITHM_SEQUENCE_FIRST + ) + + private val OPTIMIZATIONS = listOf( + OPTIMIZATION_CACHING, + OPTIMIZATION_ITERATIVE_FIXPOINT, + OPTIMIZATION_EARLY_TERMINATION + ) + } + + private val firstCache = mutableMapOf, Set>() + private val followCache = mutableMapOf>() + private var cacheHits = 0 + private var cacheMisses = 0 + + /** + * ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„์˜ FIRST ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ์ƒ์‚ฐ ๊ทœ์น™๋“ค + * @param terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ๊ฐ ๋…ผํ„ฐ๋ฏธ๋„์˜ FIRST ์ง‘ํ•ฉ ๋งต + */ + fun calculateFirstSets( + productions: List, + terminals: Set, + nonTerminals: Set + ): Map> { + val firstSets = mutableMapOf>() + + // ์ดˆ๊ธฐํ™”: ํ„ฐ๋ฏธ๋„์˜ FIRST ์ง‘ํ•ฉ์€ ์ž๊ธฐ ์ž์‹  + terminals.forEach { terminal -> + firstSets[terminal] = mutableSetOf(terminal) + } + + // ๋…ผํ„ฐ๋ฏธ๋„์˜ FIRST ์ง‘ํ•ฉ ์ดˆ๊ธฐํ™” + nonTerminals.forEach { nonTerminal -> + firstSets[nonTerminal] = mutableSetOf() + } + + // ์—ก์‹ค๋ก ๋„ ์ถ”๊ฐ€ + firstSets[TokenType.EPSILON] = mutableSetOf(TokenType.EPSILON) + + var changed = true + var iterations = 0 + + while (changed && iterations < MAX_ITERATIONS) { + changed = false + iterations++ + + for (production in productions) { + val leftSymbol = production.left + val rightSymbols = production.right + val originalSize = firstSets[leftSymbol]?.size ?: 0 + + if (rightSymbols.isEmpty()) { + // ์—ก์‹ค๋ก  ์ƒ์‚ฐ ๊ทœ์น™ + firstSets.getOrPut(leftSymbol) { mutableSetOf() }.add(TokenType.EPSILON) + } else { + // ์šฐ๋ณ€์˜ FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ + val firstOfRight = calculateFirstOfSequence(rightSymbols, firstSets) + firstSets.getOrPut(leftSymbol) { mutableSetOf() }.addAll(firstOfRight) + } + + if ((firstSets[leftSymbol]?.size ?: 0) > originalSize) { + changed = true + } + } + } + + if (iterations >= MAX_ITERATIONS) { + throw ParserException.followSetNotConverging() + } + + return firstSets.mapValues { it.value.toSet() } + } + + /** + * ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„์˜ FOLLOW ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ์ƒ์‚ฐ ๊ทœ์น™๋“ค + * @param terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @param startSymbol ์‹œ์ž‘ ์‹ฌ๋ณผ + * @param firstSets ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ๋œ FIRST ์ง‘ํ•ฉ๋“ค + * @return ๊ฐ ๋…ผํ„ฐ๋ฏธ๋„์˜ FOLLOW ์ง‘ํ•ฉ ๋งต + */ + fun calculateFollowSets( + productions: List, + terminals: Set, + nonTerminals: Set, + startSymbol: TokenType, + firstSets: Map> + ): Map> { + val followSets = mutableMapOf>() + + // ์ดˆ๊ธฐํ™”: ๋…ผํ„ฐ๋ฏธ๋„์˜ FOLLOW ์ง‘ํ•ฉ์€ ๋นˆ ์ง‘ํ•ฉ + nonTerminals.forEach { nonTerminal -> + followSets[nonTerminal] = mutableSetOf() + } + + // ์‹œ์ž‘ ์‹ฌ๋ณผ์˜ FOLLOW ์ง‘ํ•ฉ์— $ ์ถ”๊ฐ€ + followSets[startSymbol]?.add(TokenType.DOLLAR) + + var changed = true + var iterations = 0 + + while (changed && iterations < MAX_ITERATIONS) { + changed = false + iterations++ + + for (production in productions) { + val rightSymbols = production.right + + for (i in rightSymbols.indices) { + val symbol = rightSymbols[i] + + if (symbol.isNonTerminal()) { + val originalSize = followSets[symbol]?.size ?: 0 + val beta = rightSymbols.drop(i + 1) + + if (beta.isEmpty()) { + // A -> ฮฑB ํ˜•ํƒœ: FOLLOW(B)์— FOLLOW(A) ์ถ”๊ฐ€ + val followA = followSets[production.left] ?: emptySet() + followSets.getOrPut(symbol) { mutableSetOf() }.addAll(followA) + } else { + // A -> ฮฑBฮฒ ํ˜•ํƒœ: FOLLOW(B)์— FIRST(ฮฒ) ์ถ”๊ฐ€ (ฮต ์ œ์™ธ) + val firstBeta = calculateFirstOfSequence(beta, firstSets) + val firstBetaWithoutEpsilon = firstBeta - TokenType.EPSILON + + followSets.getOrPut(symbol) { mutableSetOf() }.addAll(firstBetaWithoutEpsilon) + + // ฮฒ๊ฐ€ ฮต์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉด FOLLOW(A)๋„ ์ถ”๊ฐ€ + if (TokenType.EPSILON in firstBeta) { + val followA = followSets[production.left] ?: emptySet() + followSets.getOrPut(symbol) { mutableSetOf() }.addAll(followA) + } + } + + if ((followSets[symbol]?.size ?: 0) > originalSize) { + changed = true + } + } + } + } + } + + if (iterations >= MAX_ITERATIONS) { + throw ParserException.firstSetNotConverging() + } + + return followSets.mapValues { it.value.toSet() } + } + + /** + * ์‹ฌ๋ณผ ์‹œํ€€์Šค์˜ FIRST ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbols ์‹ฌ๋ณผ ์‹œํ€€์Šค + * @param firstSets ๊ฐ ์‹ฌ๋ณผ์˜ FIRST ์ง‘ํ•ฉ + * @return ์‹œํ€€์Šค์˜ FIRST ์ง‘ํ•ฉ + */ + fun calculateFirstOfSequence( + symbols: List, + firstSets: Map> + ): Set { + val cacheKey = symbols.toList() + firstCache[cacheKey]?.let { + cacheHits++ + return it + } + + cacheMisses++ + + if (symbols.isEmpty()) { + val result = setOf(TokenType.EPSILON) + cacheFirst(cacheKey, result) + return result + } + + val result = mutableSetOf() + var allCanDeriveEpsilon = true + + for (symbol in symbols) { + val firstOfSymbol = firstSets[symbol] ?: setOf(symbol) + + // ฮต์„ ์ œ์™ธํ•œ ๋ชจ๋“  ์‹ฌ๋ณผ์„ ์ถ”๊ฐ€ + result.addAll(firstOfSymbol - TokenType.EPSILON) + + // ํ˜„์žฌ ์‹ฌ๋ณผ์ด ฮต์„ ์œ ๋„ํ•  ์ˆ˜ ์—†์œผ๋ฉด ์ค‘๋‹จ + if (TokenType.EPSILON !in firstOfSymbol) { + allCanDeriveEpsilon = false + break + } + } + + // ๋ชจ๋“  ์‹ฌ๋ณผ์ด ฮต์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉด ฮต๋„ ์ถ”๊ฐ€ + if (allCanDeriveEpsilon) { + result.add(TokenType.EPSILON) + } + + val finalResult = result.toSet() + cacheFirst(cacheKey, finalResult) + return finalResult + } + + /** + * ํŠน์ • ์‹ฌ๋ณผ์˜ FIRST ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol ๊ณ„์‚ฐํ•  ์‹ฌ๋ณผ + * @param productions ์ƒ์‚ฐ ๊ทœ์น™๋“ค + * @param firstSets ๋ฏธ๋ฆฌ ๊ณ„์‚ฐ๋œ FIRST ์ง‘ํ•ฉ๋“ค + * @return ์‹ฌ๋ณผ์˜ FIRST ์ง‘ํ•ฉ + */ + fun calculateFirstOfSymbol( + symbol: TokenType, + productions: List, + firstSets: Map> + ): Set { + return firstSets[symbol] ?: if (symbol.isTerminal) { + setOf(symbol) + } else { + calculateFirstSetsForSymbol(symbol, productions, firstSets) + } + } + + /** + * LR ์•„์ดํ…œ์˜ ๋ฒ ํƒ€ ๋ถ€๋ถ„์— ๋Œ€ํ•œ FIRST ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param beta ๋ฒ ํƒ€ ์‹œํ€€์Šค + * @param lookahead ์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ + * @param firstSets FIRST ์ง‘ํ•ฉ๋“ค + * @return ๊ณ„์‚ฐ๋œ FIRST ์ง‘ํ•ฉ + */ + fun calculateFirstOfBetaLookahead( + beta: List, + lookahead: TokenType, + firstSets: Map> + ): Set { + val firstBeta = calculateFirstOfSequence(beta, firstSets) + + return if (TokenType.EPSILON in firstBeta) { + (firstBeta - TokenType.EPSILON) + lookahead + } else { + firstBeta + } + } + + /** + * ๋…ผํ„ฐ๋ฏธ๋„์ด ์—ก์‹ค๋ก ์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param nonTerminal ํ™•์ธํ•  ๋…ผํ„ฐ๋ฏธ๋„ + * @param firstSets FIRST ์ง‘ํ•ฉ๋“ค + * @return ์—ก์‹ค๋ก ์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉด true + */ + fun canDeriveEpsilon( + nonTerminal: TokenType, + firstSets: Map> + ): Boolean { + return TokenType.EPSILON in (firstSets[nonTerminal] ?: emptySet()) + } + + /** + * ์‹ฌ๋ณผ ์‹œํ€€์Šค๊ฐ€ ์—ก์‹ค๋ก ์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbols ํ™•์ธํ•  ์‹ฌ๋ณผ ์‹œํ€€์Šค + * @param firstSets FIRST ์ง‘ํ•ฉ๋“ค + * @return ์—ก์‹ค๋ก ์„ ์œ ๋„ํ•  ์ˆ˜ ์žˆ์œผ๋ฉด true + */ + fun canSequenceDeriveEpsilon( + symbols: List, + firstSets: Map> + ): Boolean { + return symbols.all { symbol -> + TokenType.EPSILON in (firstSets[symbol] ?: emptySet()) + } + } + + /** + * FIRST ์ง‘ํ•ฉ๋“ค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param firstSets ๊ฒ€์ฆํ•  FIRST ์ง‘ํ•ฉ๋“ค + * @param terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateFirstSets( + firstSets: Map>, + terminals: Set, + nonTerminals: Set + ): Boolean { + // ํ„ฐ๋ฏธ๋„์˜ FIRST ์ง‘ํ•ฉ์€ ์ž๊ธฐ ์ž์‹ ๋งŒ ํฌํ•จํ•ด์•ผ ํ•จ + terminals.forEach { terminal -> + val firstSet = firstSets[terminal] ?: return false + if (firstSet != setOf(terminal)) return false + } + + // ๋…ผํ„ฐ๋ฏธ๋„์˜ FIRST ์ง‘ํ•ฉ์€ ํ„ฐ๋ฏธ๋„๊ณผ ์—ก์‹ค๋ก ๋งŒ ํฌํ•จํ•ด์•ผ ํ•จ + nonTerminals.forEach { nonTerminal -> + val firstSet = firstSets[nonTerminal] ?: return false + val validSymbols = terminals + TokenType.EPSILON + if (!firstSet.all { it in validSymbols }) return false + } + + return true + } + + /** + * FOLLOW ์ง‘ํ•ฉ๋“ค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param followSets ๊ฒ€์ฆํ•  FOLLOW ์ง‘ํ•ฉ๋“ค + * @param terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun validateFollowSets( + followSets: Map>, + terminals: Set, + nonTerminals: Set + ): Boolean { + // ๋…ผํ„ฐ๋ฏธ๋„์˜ FOLLOW ์ง‘ํ•ฉ์€ ํ„ฐ๋ฏธ๋„๊ณผ $๋งŒ ํฌํ•จํ•ด์•ผ ํ•จ + nonTerminals.forEach { nonTerminal -> + val followSet = followSets[nonTerminal] ?: return false + val validSymbols = terminals + TokenType.DOLLAR + if (!followSet.all { it in validSymbols }) return false + } + + return true + } + + /** + * ์บ์‹œ๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + fun clearCache() { + firstCache.clear() + followCache.clear() + cacheHits = 0 + cacheMisses = 0 + } + + /** + * ์บ์‹œ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์บ์‹œ ํ†ต๊ณ„ ์ •๋ณด + */ + fun getCacheStatistics(): Map { + val totalRequests = cacheHits + cacheMisses + val hitRate = if (totalRequests > 0) cacheHits.toDouble() / totalRequests else 0.0 + + return mapOf( + "firstCache" to mapOf( + "size" to firstCache.size, + "hits" to cacheHits, + "misses" to cacheMisses, + "hitRate" to hitRate + ), + "followCache" to mapOf( + "size" to followCache.size + ), + "totalRequests" to totalRequests + ) + } + + // Private helper methods + + private fun calculateFirstSetsForSymbol( + symbol: TokenType, + productions: List, + firstSets: Map> + ): Set { + val result = mutableSetOf() + val symbolProductions = productions.filter { it.left == symbol } + + for (production in symbolProductions) { + if (production.right.isEmpty()) { + result.add(TokenType.EPSILON) + } else { + val firstOfRight = calculateFirstOfSequence(production.right, firstSets) + result.addAll(firstOfRight) + } + } + + return result + } + + private fun cacheFirst(key: List, value: Set) { + if (firstCache.size >= CACHE_SIZE_LIMIT) { + // ์บ์‹œ ํฌ๊ธฐ ์ œํ•œ: ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ํ•ญ๋ชฉ ์ œ๊ฑฐ + val oldestKey = firstCache.keys.first() + firstCache.remove(oldestKey) + } + firstCache[key] = value + } + + /** + * ์„œ๋น„์Šค์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + KEY_MAX_ITERATIONS to MAX_ITERATIONS, + KEY_CACHE_SIZE_LIMIT to CACHE_SIZE_LIMIT, + KEY_ALGORITHMS to ALGORITHMS, + KEY_OPTIMIZATIONS to OPTIMIZATIONS + ) + + /** + * ์„œ๋น„์Šค ์‚ฌ์šฉ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + KEY_SERVICE_NAME to SERVICE_NAME, + KEY_CACHE_STATISTICS to getCacheStatistics(), + KEY_ALGORITHMS_IMPLEMENTED to ALGORITHMS_COUNT + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/LRParserTableService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/LRParserTableService.kt new file mode 100644 index 00000000..8a66bea2 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/LRParserTableService.kt @@ -0,0 +1,585 @@ +package hs.kr.entrydsm.domain.parser.services + +import hs.kr.entrydsm.global.extensions.* + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.domain.parser.entities.ParsingState +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.factories.LRItemFactory +import hs.kr.entrydsm.domain.parser.factories.ParsingStateFactory +import hs.kr.entrydsm.domain.parser.values.Grammar +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.parser.values.ParsingTable +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType +import hs.kr.entrydsm.global.configuration.ParserConfiguration +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider + +/** + * LR ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ตฌ์ถ•์„ ๋‹ด๋‹นํ•˜๋Š” ๋„๋ฉ”์ธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Domain Service ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ LR(1) ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ๋ณต์žกํ•œ ๊ตฌ์ถ• ๋กœ์ง์„ + * ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. ์ƒํƒœ ์ƒ์„ฑ, ์ „์ด ๊ณ„์‚ฐ, ์•ก์…˜/goto ํ…Œ์ด๋ธ” ๊ตฌ์„ฑ, ์ถฉ๋Œ ํ•ด๊ฒฐ ๋“ฑ + * LR ํŒŒ์„œ์˜ ํ•ต์‹ฌ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Service( + name = "LRParserTableService", + type = ServiceType.DOMAIN_SERVICE +) +class LRParserTableService( + private val lrItemFactory: LRItemFactory, + private val parsingStateFactory: ParsingStateFactory, + private val firstFollowCalculatorService: FirstFollowCalculatorService, + private val configurationProvider: ConfigurationProvider +) { + + companion object { + private const val MAX_MERGE_ITERATIONS = 50 // ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ + private const val MAX_QUEUE_REINSERTIONS = 20 // ํ ์žฌ์‚ฝ์ž… ์ œํ•œ + + private const val UNKNOWN_ERROR = "Unknown error" + + // Configuration keys + private const val KEY_MAX_STATES = "maxStates" + private const val KEY_MAX_ITEMS_PER_STATE = "maxItemsPerState" + private const val KEY_CACHING_ENABLED = "cachingEnabled" + private const val KEY_PARSING_STRATEGY = "parsingStrategy" + private const val KEY_OPTIMIZATIONS = "optimizations" + + // Statistics keys + private const val KEY_SERVICE_NAME = "serviceName" + private const val KEY_CACHE_STATISTICS = "cacheStatistics" + private const val KEY_ALGORITHMS_IMPLEMENTED = "algorithmsImplemented" + + // Strategy names + private const val PARSING_STRATEGY_LR1 = "LR(1)" + + // Optimization names + private const val OPTIMIZATION_STATE_COMPRESSION = "stateCompression" + private const val OPTIMIZATION_CACHING = "caching" + private const val OPTIMIZATION_CONFLICT_DETECTION = "conflictDetection" + + // Algorithm names + private const val ALGORITHM_LR1_STATE_CONSTRUCTION = "LR1StateConstruction" + private const val ALGORITHM_TABLE_GENERATION = "TableGeneration" + private const val ALGORITHM_CONFLICT_DETECTION = "ConflictDetection" + + // Service info + private const val SERVICE_NAME = "LRParserTableService" + private const val STACK_DEPTH_RATIO = 10 + + // Collections + private val OPTIMIZATIONS_LIST = listOf( + OPTIMIZATION_STATE_COMPRESSION, + OPTIMIZATION_CACHING, + OPTIMIZATION_CONFLICT_DETECTION + ) + + private val ALGORITHMS_IMPLEMENTED = listOf( + ALGORITHM_LR1_STATE_CONSTRUCTION, + ALGORITHM_TABLE_GENERATION, + ALGORITHM_CONFLICT_DETECTION + ) + } + + // ์„ค์ •์€ ConfigurationProvider๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ์ ‘๊ทผ + private val config: ParserConfiguration + get() = configurationProvider.getParserConfiguration() + + private val stateCache = mutableMapOf, ParsingState>() + private val tableCache = mutableMapOf() + private var cacheHits = 0 + private var cacheMisses = 0 + + // ์ƒํƒœ ๋ณ‘ํ•ฉ ์ถ”์ ์„ ์œ„ํ•œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ + private val stateReinsertionCount = mutableMapOf() // ์ƒํƒœ๋ณ„ ์žฌ์‚ฝ์ž… ํšŸ์ˆ˜ + private val mergeHistory = mutableMapOf() // ์ƒํƒœ๋ณ„ ๋ณ‘ํ•ฉ ํšŸ์ˆ˜ + + /** + * ์ฃผ์–ด์ง„ ๋ฌธ๋ฒ•์œผ๋กœ๋ถ€ํ„ฐ LR(1) ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + * + * @param grammar ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ๊ตฌ์ถ•ํ•  ๋ฌธ๋ฒ• + * @return ๊ตฌ์ถ•๋œ LR(1) ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” + */ + fun buildParsingTable(grammar: Grammar): ParsingTable { + val cacheKey = generateGrammarCacheKey(grammar) + + tableCache[cacheKey]?.let { + cacheHits++ + return it + } + + cacheMisses++ + + // ์ƒํƒœ ๋ณ‘ํ•ฉ ์ถ”์  ๋ฐ์ดํ„ฐ ์ดˆ๊ธฐํ™” (๊ฐ ํ…Œ์ด๋ธ” ๋นŒ๋“œ๋งˆ๋‹ค ๋ฆฌ์…‹) + stateReinsertionCount.clear() + mergeHistory.clear() + + val productions = grammar.productions + grammar.augmentedProduction + val firstSets = firstFollowCalculatorService.calculateFirstSets( + productions, grammar.terminals, grammar.nonTerminals + ) + + val states = buildLR1States(productions, grammar.startSymbol, firstSets) + val parsingTable = constructParsingTable(states, productions, grammar.terminals, grammar.nonTerminals) + + // ์บ์‹œ ํฌ๊ธฐ ์ œํ•œ + if (tableCache.size >= (config.maxTokenCount / 500)) { // ๋Œ€๋žต์  ์บ์‹œ ํฌ๊ธฐ + clearOldestCacheEntry() + } + + tableCache[cacheKey] = parsingTable + return parsingTable + } + + /** + * LR(1) ์ƒํƒœ๋“ค์„ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™๋“ค (ํ™•์žฅ ๊ทœ์น™ ํฌํ•จ) + * @param startSymbol ์‹œ์ž‘ ์‹ฌ๋ณผ + * @param firstSets FIRST ์ง‘ํ•ฉ๋“ค + * @return ๊ตฌ์ถ•๋œ ์ƒํƒœ๋“ค์˜ ๋งต + */ + fun buildLR1States( + productions: List, + startSymbol: TokenType, + firstSets: Map> + ): Map { + val states = mutableMapOf() + val stateQueue = mutableListOf() + val kernelToStateMap = mutableMapOf, Int>() + + // ์ดˆ๊ธฐ ์ƒํƒœ ์ƒ์„ฑ + val startProduction = productions.find { it.id == -1 } + ?: throw ParserException.augmentedProductionNotFound() + + val startItem = lrItemFactory.createStartItem(startProduction) + val initialState = parsingStateFactory.createStateWithClosure( + setOf(startItem), productions, firstSets + ) + + states[0] = initialState + stateQueue.add(initialState) + kernelToStateMap[initialState.getKernelItems()] = 0 + + var stateIdCounter = 1 + + while (stateQueue.isNotEmpty() && states.size < config.maxParsingSteps) { + val currentState = stateQueue.removeAt(0) + val transitions = mutableMapOf() + val actions = mutableMapOf() + val gotos = mutableMapOf() + + // ๋ชจ๋“  ๊ฐ€๋Šฅํ•œ ์‹ฌ๋ณผ์— ๋Œ€ํ•ด GOTO ์—ฐ์‚ฐ ์ˆ˜ํ–‰ + val symbols = collectTransitionSymbols(currentState) + + for (symbol in symbols) { + val gotoState = parsingStateFactory.createStateWithGoto( + currentState, symbol, productions, firstSets + ) + + if (gotoState != null) { + val kernelItems = gotoState.getKernelItems() + val existingStateId = kernelToStateMap[kernelItems] + + val targetStateId = if (existingStateId != null) { + handleStateMerging(existingStateId, gotoState, states, stateQueue) + existingStateId + } else { + // ์ƒˆ๋กœ์šด ์ƒํƒœ ์ถ”๊ฐ€ + val newStateId = stateIdCounter++ + val newState = gotoState.copy(id = newStateId) + states[newStateId] = newState + stateQueue.add(newState) + kernelToStateMap[kernelItems] = newStateId + newStateId + } + + transitions[symbol] = targetStateId + + if (symbol.isTerminal) { + actions[symbol] = LRAction.Shift(targetStateId) + } else { + gotos[symbol] = targetStateId + } + } + } + + // Reduce ์•ก์…˜ ์ถ”๊ฐ€ + addReduceActions(currentState, actions, productions) + + // Accept ์•ก์…˜ ์ถ”๊ฐ€ + addAcceptAction(currentState, actions, startProduction) + + // ์ƒํƒœ ์—…๋ฐ์ดํŠธ + val updatedState = currentState.copy( + transitions = transitions, + actions = actions, + gotos = gotos + ) + states[currentState.id] = updatedState + } + + return states + } + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param states LR(1) ์ƒํƒœ๋“ค + * @param productions ์ƒ์‚ฐ ๊ทœ์น™๋“ค + * @param terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค + * @return ๊ตฌ์„ฑ๋œ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” + */ + fun constructParsingTable( + states: Map, + productions: List, + terminals: Set, + nonTerminals: Set + ): ParsingTable { + val actionTable = mutableMapOf, LRAction>() + val gotoTable = mutableMapOf, Int>() + val acceptStates = mutableSetOf() + + states.values.forEach { state -> + // Action ํ…Œ์ด๋ธ” ๊ตฌ์„ฑ + state.actions.forEach { (terminal, action) -> + actionTable[state.id to terminal] = action + } + + // Goto ํ…Œ์ด๋ธ” ๊ตฌ์„ฑ + state.gotos.forEach { (nonTerminal, targetState) -> + gotoTable[state.id to nonTerminal] = targetState + } + + // Accept ์ƒํƒœ ์ˆ˜์ง‘ + if (state.isAccepting) { + acceptStates.add(state.id) + } + } + + return ParsingTable( + states = states, + actionTable = actionTable, + gotoTable = gotoTable, + startState = 0, + acceptStates = acceptStates, + terminals = terminals, + nonTerminals = nonTerminals + ) + } + + /** + * ํ…Œ์ด๋ธ” ๊ตฌ์ถ•์ด ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param grammar ํ™•์ธํ•  ๋ฌธ๋ฒ• + * @return ๊ตฌ์ถ• ๊ฐ€๋Šฅํ•˜๋ฉด true + */ + fun canBuildParsingTable(grammar: Grammar): Boolean { + return try { + buildParsingTable(grammar) + true + } catch (e: Exception) { + false + } + } + + /** + * ํ…Œ์ด๋ธ” ๊ตฌ์ถ• ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ถฉ๋Œ์„ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param grammar ๋ถ„์„ํ•  ๋ฌธ๋ฒ• + * @return ์ถฉ๋Œ ๋ถ„์„ ๊ฒฐ๊ณผ + */ + fun analyzeConflicts(grammar: Grammar): Map { + return try { + val parsingTable = buildParsingTable(grammar) + val conflicts = parsingTable.getConflicts() + + mapOf( + "hasConflicts" to conflicts.isNotEmpty(), + "conflictTypes" to conflicts.keys, + "conflictDetails" to conflicts, + "stateCount" to parsingTable.states.size, + "tableSize" to parsingTable.getSizeInfo() + ) + } catch (e: Exception) { + mapOf( + "error" to (e.message ?: UNKNOWN_ERROR), + "buildingFailed" to true + ) + } + } + + /** + * ์ƒํƒœ ์••์ถ•์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param states ์••์ถ•ํ•  ์ƒํƒœ๋“ค + * @return ์••์ถ•๋œ ์ƒํƒœ๋“ค + */ + fun compressStates(states: Map): Map { + // ๋™์ผํ•œ ์ปค๋„ ์•„์ดํ…œ์„ ๊ฐ€์ง„ ์ƒํƒœ๋“ค์„ ๋ณ‘ํ•ฉ + val kernelGroups = states.values.groupBy { it.getKernelItems() } + val compressedStates = mutableMapOf() + var newStateId = 0 + + kernelGroups.values.forEach { stateGroup -> + if (stateGroup.size == 1) { + compressedStates[newStateId] = stateGroup.first().copy(id = newStateId) + } else { + val mergedState = parsingStateFactory.mergeStates(stateGroup) + if (mergedState != null) { + compressedStates[newStateId] = mergedState.copy(id = newStateId) + } + } + newStateId++ + } + + return compressedStates + } + + /** + * ์บ์‹œ๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + fun clearCache() { + stateCache.clear() + tableCache.clear() + cacheHits = 0 + cacheMisses = 0 + } + + /** + * ์บ์‹œ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์บ์‹œ ํ†ต๊ณ„ ์ •๋ณด + */ + fun getCacheStatistics(): Map = mapOf( + "stateCache" to mapOf( + "size" to stateCache.size, + "hits" to cacheHits, + "misses" to cacheMisses, + "hitRate" to if (cacheHits + cacheMisses > 0) cacheHits.toDouble() / (cacheHits + cacheMisses) else 0.0 + ), + "tableCache" to mapOf( + "size" to tableCache.size, + "limit" to (config.maxTokenCount / 500) + ) + ) + + // Private helper methods + + private fun generateGrammarCacheKey(grammar: Grammar): String { + // ๋ฌธ๋ฒ•์˜ ํ•ด์‹œ๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์บ์‹œ ํ‚ค ์ƒ์„ฑ + val productionHashes = grammar.productions.map { + "${it.id}:${it.left}:${it.right.joinToString(",")}" + } + return productionHashes.joinToString("|").hashCode().toString() + } + + private fun clearOldestCacheEntry() { + if (tableCache.isNotEmpty()) { + val oldestKey = tableCache.keys.first() + tableCache.remove(oldestKey) + } + } + + private fun collectTransitionSymbols(state: ParsingState): Set { + return state.items.mapNotNull { it.nextSymbol() }.toSet() + } + + private fun addReduceActions( + state: ParsingState, + actions: MutableMap, + productions: List + ) { + state.items.filter { it.isComplete() }.forEach { item -> + val lookahead = item.lookahead + val existingAction = actions[lookahead] + if (existingAction != null) { + throw ParserException.lrConflictDetected( + lookahead = lookahead, + stateId = state.id + ) + } else { + actions[lookahead] = LRAction.Reduce(item.production) + } + } + } + + private fun addAcceptAction( + state: ParsingState, + actions: MutableMap, + startProduction: Production + ) { + val acceptItems = state.items.filter { item -> + item.production.id == startProduction.id && + item.isComplete() && + item.lookahead == TokenType.DOLLAR + } + + if (acceptItems.isNotEmpty()) { + actions[TokenType.DOLLAR] = LRAction.Accept + } + } + + /** + * ์„œ๋น„์Šค์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + KEY_MAX_STATES to config.maxParsingSteps, + KEY_MAX_ITEMS_PER_STATE to config.maxStackDepth / STACK_DEPTH_RATIO, // ๋Œ€๋žต์  ๋น„์œจ + KEY_CACHING_ENABLED to config.cachingEnabled, + KEY_PARSING_STRATEGY to PARSING_STRATEGY_LR1, + KEY_OPTIMIZATIONS to if (config.enableOptimizations) OPTIMIZATIONS_LIST else emptyList() + ) + + /** + * ์„œ๋น„์Šค ์‚ฌ์šฉ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + KEY_SERVICE_NAME to SERVICE_NAME, + KEY_CACHE_STATISTICS to getCacheStatistics(), + KEY_ALGORITHMS_IMPLEMENTED to ALGORITHMS_IMPLEMENTED + ) + + /** + * ์ƒํƒœ ๋ณ‘ํ•ฉ์„ ์•ˆ์ „ํ•˜๊ณ  ์™„์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * ์ด ๋ฉ”์„œ๋“œ๋Š” ๋‹ค์Œ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค: + * 1. ๋ณ‘ํ•ฉ๋œ ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ํ์— ๋‹ค์‹œ ์ถ”๊ฐ€ + * 2. ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ + * 3. ์„ฑ๋Šฅ ์ตœ์ ํ™” (๋ถˆํ•„์š”ํ•œ ์žฌ์‚ฝ์ž… ๋ฐฉ์ง€) + * 4. LALR(1) ์•Œ๊ณ ๋ฆฌ์ฆ˜ ํ‘œ์ค€ ์ค€์ˆ˜ + * + * @param existingStateId ๊ธฐ์กด ์ƒํƒœ์˜ ID + * @param newState ๋ณ‘ํ•ฉํ•  ์ƒˆ๋กœ์šด ์ƒํƒœ + * @param states ์ƒํƒœ ๋งต + * @param stateQueue ์ฒ˜๋ฆฌํ•  ์ƒํƒœ๋“ค์˜ ํ + */ + private fun handleStateMerging( + existingStateId: Int, + newState: ParsingState, + states: MutableMap, + stateQueue: MutableList + ) { + val existingState = states[existingStateId]!! + + // 1. ๋ฌดํ•œ ๋ฃจํ”„ ๋ฐฉ์ง€ - ์žฌ์‚ฝ์ž… ํšŸ์ˆ˜ ์ฒดํฌ + val currentReinsertions = stateReinsertionCount.getOrDefault(existingStateId, 0) + if (currentReinsertions >= MAX_QUEUE_REINSERTIONS) { + // ์ตœ๋Œ€ ์žฌ์‚ฝ์ž… ํšŸ์ˆ˜ ์ดˆ๊ณผ ์‹œ ๊ฒฝ๊ณ  ๋กœ๊ทธ์™€ ํ•จ๊ป˜ ์•ˆ์ „ํ•˜๊ฒŒ ์ข…๋ฃŒ + return + } + + // 2. ๋ณ‘ํ•ฉ ์ „ ์ƒํƒœ ์ €์žฅ (๋ณ€๊ฒฝ ๊ฐ์ง€๋ฅผ ์œ„ํ•ด) + val originalItemsCount = existingState.items.size + val originalLookaheads = existingState.items.map { it.lookahead }.toSet() + + // 3. ์ƒํƒœ ๋ณ‘ํ•ฉ ์ˆ˜ํ–‰ + val mergedState = parsingStateFactory.mergeStates(listOf(existingState, newState)) + + if (mergedState != null) { + // 4. ๋ณ€๊ฒฝ ๊ฐ์ง€ - ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•์œผ๋กœ ๋ณ€๊ฒฝ ์—ฌ๋ถ€ ํ™•์ธ + val hasChanged = detectStateChanges( + originalItemsCount, + originalLookaheads, + mergedState + ) + + if (hasChanged) { + // 5. ์ƒํƒœ ์—…๋ฐ์ดํŠธ + states[existingStateId] = mergedState + + // 6. ๋ณ‘ํ•ฉ ํšŸ์ˆ˜ ์ถ”์  + mergeHistory[existingStateId] = mergeHistory.getOrDefault(existingStateId, 0) + 1 + + // 7. ํ์— ๋‹ค์‹œ ์ถ”๊ฐ€ (์ค‘๋ณต ์ฒดํฌ์™€ ํ•จ๊ป˜) + if (shouldReinsertToQueue(existingStateId, mergedState, stateQueue)) { + stateQueue.add(mergedState) + stateReinsertionCount[existingStateId] = currentReinsertions + 1 + } + } + } + } + + /** + * ์ƒํƒœ์˜ ๋ณ€๊ฒฝ์„ ํšจ์œจ์ ์œผ๋กœ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param originalItemsCount ์›๋ž˜ ์•„์ดํ…œ ๊ฐœ์ˆ˜ + * @param originalLookaheads ์›๋ž˜ lookahead ์‹ฌ๋ณผ๋“ค + * @param mergedState ๋ณ‘ํ•ฉ๋œ ์ƒํƒœ + * @return ์ƒํƒœ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ์œผ๋ฉด true + */ + private fun detectStateChanges( + originalItemsCount: Int, + originalLookaheads: Set, + mergedState: ParsingState + ): Boolean { + // 1. ์•„์ดํ…œ ๊ฐœ์ˆ˜ ๋ณ€๊ฒฝ ์ฒดํฌ (๊ฐ€์žฅ ๋น ๋ฅธ ์ฒดํฌ) + if (mergedState.items.size != originalItemsCount) { + return true + } + + // 2. Lookahead ์‹ฌ๋ณผ ๋ณ€๊ฒฝ ์ฒดํฌ + val newLookaheads = mergedState.items.map { it.lookahead }.toSet() + if (newLookaheads != originalLookaheads) { + return true + } + + return false + } + + /** + * ์ƒํƒœ๋ฅผ ํ์— ๋‹ค์‹œ ์‚ฝ์ž…ํ• ์ง€ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param stateId ์ƒํƒœ ID + * @param state ์ƒํƒœ + * @param queue ํ + * @return ์žฌ์‚ฝ์ž…ํ•ด์•ผ ํ•˜๋ฉด true + */ + private fun shouldReinsertToQueue( + stateId: Int, + state: ParsingState, + queue: MutableList + ): Boolean { + if (queue.any { it.id == stateId }) { + return false + } + + val mergeCount = mergeHistory.getOrDefault(stateId, 0) + if (mergeCount > MAX_MERGE_ITERATIONS) { + return false + } + + if (queue.size > config.maxParsingSteps / 2) { + return false + } + + return true + } + + /** + * ์ƒํƒœ ๋ณ‘ํ•ฉ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (๋””๋ฒ„๊น… ๋ฐ ๋ชจ๋‹ˆํ„ฐ๋ง์šฉ) + */ + fun getMergeStatistics(): Map { + return mapOf( + "totalMerges" to mergeHistory.values.sum(), + "totalReinsertions" to stateReinsertionCount.values.sum(), + "averageMergesPerState" to if (mergeHistory.isNotEmpty()) + mergeHistory.values.average() else 0.0, + "maxMergesForSingleState" to (mergeHistory.values.maxOrNull() ?: 0), + "statesWithMerges" to mergeHistory.size + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/OptimizedParsingTable.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/OptimizedParsingTable.kt new file mode 100644 index 00000000..07a34019 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/OptimizedParsingTable.kt @@ -0,0 +1,478 @@ +package hs.kr.entrydsm.domain.parser.services + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType + +/** + * 2D ๋ฐฐ์—ด๋กœ ์ตœ์ ํ™”๋œ LR ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ์ œ๊ณตํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋งต ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ”๋ณด๋‹ค ํ›จ์”ฌ ๋น ๋ฅธ ์ ‘๊ทผ ์†๋„์™€ ๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ์„ ์ œ๊ณตํ•˜๋ฉฐ, + * ํŒŒ์‹ฑ ์„ฑ๋Šฅ์„ ๋Œ€ํญ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ 2D ๋ฐฐ์—ด ์ตœ์ ํ™”๋ฅผ DDD ๊ตฌ์กฐ๋กœ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @property actionTable2D 2D ์•ก์…˜ ํ…Œ์ด๋ธ” [์ƒํƒœ][ํ„ฐ๋ฏธ๋„] + * @property gotoTable2D 2D GOTO ํ…Œ์ด๋ธ” [์ƒํƒœ][๋…ผํ„ฐ๋ฏธ๋„] + * @property terminalToIndex ํ„ฐ๋ฏธ๋„ -> ์ธ๋ฑ์Šค ๋งคํ•‘ + * @property nonTerminalToIndex ๋…ผํ„ฐ๋ฏธ๋„ -> ์ธ๋ฑ์Šค ๋งคํ•‘ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Service( + name = "OptimizedParsingTable", + type = ServiceType.DOMAIN_SERVICE +) +class OptimizedParsingTable private constructor( + private val actionTable2D: Array>, + private val gotoTable2D: Array, + private val terminalToIndex: Map, + private val nonTerminalToIndex: Map, + private val numStates: Int, + private val numTerminals: Int, + private val numNonTerminals: Int +) { + /** + * ์ฃผ์–ด์ง„ ์ƒํƒœ์™€ ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ ํŒŒ์‹ฑ ์•ก์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ํ˜„์žฌ ์ƒํƒœ ID + * @param terminal ์ž…๋ ฅ ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ์•ก์…˜ (Shift, Reduce, Accept, Error) + */ + fun getAction(state: Int, terminal: TokenType): LRAction { + if (state < 0 || state >= numStates) { + return ERROR_ACTION + } + + val terminalIndex = terminalToIndex[terminal] + ?: return ERROR_ACTION + + if (terminalIndex < 0 || terminalIndex >= numTerminals) { + return ERROR_ACTION + } + + return actionTable2D[state][terminalIndex] ?: ERROR_ACTION + } + + /** + * ์ฃผ์–ด์ง„ ์ƒํƒœ์™€ ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ GOTO ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ํ˜„์žฌ ์ƒํƒœ ID + * @param nonTerminal ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @return ๋‹ค์Œ ์ƒํƒœ ID ๋˜๋Š” null (ํ•ด๋‹นํ•˜๋Š” GOTO ์—”ํŠธ๋ฆฌ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ) + */ + fun getGoto(state: Int, nonTerminal: TokenType): Int? { + if (state < 0 || state >= numStates) { + return null + } + + val nonTerminalIndex = nonTerminalToIndex[nonTerminal] + ?: return null + + if (nonTerminalIndex < 0 || nonTerminalIndex >= numNonTerminals) { + return null + } + + val result = gotoTable2D[state][nonTerminalIndex] + return if (result == EMPTY_GOTO_ENTRY) null else result + } + + /** + * ์•ก์…˜ ํ…Œ์ด๋ธ”์— ์—”ํŠธ๋ฆฌ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ƒํƒœ ID + * @param terminal ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @param action ์„ค์ •ํ•  ์•ก์…˜ + * @return ์„ค์ • ์„ฑ๊ณต ์—ฌ๋ถ€ + */ + fun setAction(state: Int, terminal: TokenType, action: LRAction): Boolean { + if (state < 0 || state >= numStates) { + return false + } + + val terminalIndex = terminalToIndex[terminal] + ?: return false + + if (terminalIndex < 0 || terminalIndex >= numTerminals) { + return false + } + + actionTable2D[state][terminalIndex] = action + return true + } + + /** + * GOTO ํ…Œ์ด๋ธ”์— ์—”ํŠธ๋ฆฌ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ƒํƒœ ID + * @param nonTerminal ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + * @param nextState ๋‹ค์Œ ์ƒํƒœ ID + * @return ์„ค์ • ์„ฑ๊ณต ์—ฌ๋ถ€ + */ + fun setGoto(state: Int, nonTerminal: TokenType, nextState: Int): Boolean { + if (state < 0 || state >= numStates || nextState < 0) { + return false + } + + val nonTerminalIndex = nonTerminalToIndex[nonTerminal] + ?: return false + + if (nonTerminalIndex < 0 || nonTerminalIndex >= numNonTerminals) { + return false + } + + gotoTable2D[state][nonTerminalIndex] = nextState + return true + } + + /** + * ํŠน์ • ์ƒํƒœ์˜ ๋ชจ๋“  ์•ก์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ƒํƒœ ID + * @return ํ„ฐ๋ฏธ๋„ -> ์•ก์…˜ ๋งคํ•‘ + */ + fun getStateActions(state: Int): Map { + if (state < 0 || state >= numStates) { + return emptyMap() + } + + val result = mutableMapOf() + for ((terminal, index) in terminalToIndex) { + val action = actionTable2D[state][index] + if (action != null) { + result[terminal] = action + } + } + return result + } + + /** + * ํŠน์ • ์ƒํƒœ์˜ ๋ชจ๋“  GOTO๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ƒํƒœ ID + * @return ๋…ผํ„ฐ๋ฏธ๋„ -> ์ƒํƒœ ๋งคํ•‘ + */ + fun getStateGotos(state: Int): Map { + if (state < 0 || state >= numStates) { + return emptyMap() + } + + val result = mutableMapOf() + for ((nonTerminal, index) in nonTerminalToIndex) { + val nextState = gotoTable2D[state][index] + if (nextState != EMPTY_GOTO_ENTRY) { + result[nonTerminal] = nextState + } + } + return result + } + + /** + * ํ…Œ์ด๋ธ”์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ •๋ณด ๋งต + */ + fun getMemoryStats(): Map { + val actionTableSize = numStates * numTerminals + val gotoTableSize = numStates * numNonTerminals + val totalEntries = actionTableSize + gotoTableSize + + var nonNullActions = 0 + for (state in 0 until numStates) { + for (terminal in 0 until numTerminals) { + if (actionTable2D[state][terminal] != null) { + nonNullActions++ + } + } + } + + var nonEmptyGotos = 0 + for (state in 0 until numStates) { + for (nonTerminal in 0 until numNonTerminals) { + if (gotoTable2D[state][nonTerminal] != EMPTY_GOTO_ENTRY) { + nonEmptyGotos++ + } + } + } + + return mapOf( + "numStates" to numStates, + "numTerminals" to numTerminals, + "numNonTerminals" to numNonTerminals, + "actionTableSize" to actionTableSize, + "gotoTableSize" to gotoTableSize, + "totalEntries" to totalEntries, + "nonNullActions" to nonNullActions, + "nonEmptyGotos" to nonEmptyGotos, + "actionDensity" to if (actionTableSize > 0) { + nonNullActions.toDouble() / actionTableSize + } else 0.0, + "gotoDensity" to if (gotoTableSize > 0) { + nonEmptyGotos.toDouble() / gotoTableSize + } else 0.0, + "estimatedMemoryBytes" to estimateMemoryUsage() + ) + } + + /** + * ํ…Œ์ด๋ธ”์˜ ์••์ถ•๋ฅ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์••์ถ•๋ฅ  ์ •๋ณด + */ + fun getCompressionStats(): Map { + val stats = getMemoryStats() + val actionDensity = stats["actionDensity"] as Double + val gotoDensity = stats["gotoDensity"] as Double + val overallDensity = (actionDensity + gotoDensity) / 2 + + return mapOf( + "actionDensity" to actionDensity, + "gotoDensity" to gotoDensity, + "overallDensity" to overallDensity, + "compressionPotential" to (1.0 - overallDensity), + "sparsity" to (1.0 - overallDensity), + "efficiency" to if (overallDensity > 0.5) "High" else if (overallDensity > 0.2) "Medium" else "Low" + ) + } + + /** + * ํŠน์ • ํ„ฐ๋ฏธ๋„์˜ ์•ก์…˜ ๋ถ„ํฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param terminal ๋ถ„์„ํ•  ํ„ฐ๋ฏธ๋„ + * @return ์•ก์…˜ ํƒ€์ž…๋ณ„ ๊ฐœ์ˆ˜ + */ + fun getActionDistribution(terminal: TokenType): Map { + val terminalIndex = terminalToIndex[terminal] ?: return emptyMap() + + val distribution = mutableMapOf() + for (state in 0 until numStates) { + val action = actionTable2D[state][terminalIndex] + when (action) { + is LRAction.Shift -> distribution[ACTION_SHIFT] = distribution.getOrDefault(ACTION_SHIFT, 0) + 1 + is LRAction.Reduce -> distribution[ACTION_REDUCE] = distribution.getOrDefault(ACTION_REDUCE, 0) + 1 + is LRAction.Accept -> distribution[ACTION_ACCEPT] = distribution.getOrDefault(ACTION_ACCEPT, 0) + 1 + is LRAction.Error -> distribution[ACTION_ERROR] = distribution.getOrDefault(ACTION_ERROR, 0) + 1 + null -> distribution[ACTION_EMPTY] = distribution.getOrDefault(ACTION_EMPTY, 0) + 1 + } + } + return distribution + } + + /** + * ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ถ”์ • ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ (๋ฐ”์ดํŠธ) + */ + private fun estimateMemoryUsage(): Long { + // ๋Œ€๋žต์ ์ธ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ๊ณ„์‚ฐ + val actionTableBytes = numStates * numTerminals * 8L // ์ฐธ์กฐ ํฌ๊ธฐ + val gotoTableBytes = numStates * numNonTerminals * 4L // Int ํฌ๊ธฐ + val mappingBytes = (terminalToIndex.size + nonTerminalToIndex.size) * 32L // ๋งต ์˜ค๋ฒ„ํ—ค๋“œ + + return actionTableBytes + gotoTableBytes + mappingBytes + } + + /** + * ํ…Œ์ด๋ธ”์„ ๋งต ๊ธฐ๋ฐ˜์œผ๋กœ ๋‚ด๋ณด๋ƒ…๋‹ˆ๋‹ค. + * + * @return ๋งต ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ” ํ‘œํ˜„ + */ + fun exportToMaps(): TableExport { + val actionMap = mutableMapOf, LRAction>() + val gotoMap = mutableMapOf, Int>() + + for (state in 0 until numStates) { + for ((terminal, index) in terminalToIndex) { + val action = actionTable2D[state][index] + if (action != null) { + actionMap[Pair(state, terminal)] = action + } + } + + for ((nonTerminal, index) in nonTerminalToIndex) { + val nextState = gotoTable2D[state][index] + if (nextState != EMPTY_GOTO_ENTRY) { + gotoMap[Pair(state, nonTerminal)] = nextState + } + } + } + + return TableExport(actionMap, gotoMap) + } + + companion object { + + private const val ACTION_SHIFT = "Shift" + private const val ACTION_REDUCE = "Reduce" + private const val ACTION_ACCEPT = "Accept" + private const val ACTION_ERROR = "Error" + private const val ACTION_EMPTY = "Empty" + + private const val EMPTY_GOTO_ENTRY = -1 + + // Error ์•ก์…˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์‹ฑ๊ธ€ํ„ด์œผ๋กœ ์žฌ์‚ฌ์šฉ + private val ERROR_ACTION = LRAction.Error() + + /** + * Kotlin DSL์„ ์‚ฌ์šฉํ•˜์—ฌ OptimizedParsingTable์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param block DSL ๊ตฌ์„ฑ ๋ธ”๋ก + * @return ์ตœ์ ํ™”๋œ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” + */ + fun build(block: Builder.() -> Unit): OptimizedParsingTable { + return Builder().apply(block).build() + } + + /** + * ๋งต ๊ธฐ๋ฐ˜ ํ…Œ์ด๋ธ”๋กœ๋ถ€ํ„ฐ 2D ๋ฐฐ์—ด ํ…Œ์ด๋ธ”์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param actionMap ์•ก์…˜ ๋งต + * @param gotoMap GOTO ๋งต + * @param terminals ํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ + * @param numStates ์ƒํƒœ ๊ฐœ์ˆ˜ + * @return ์ตœ์ ํ™”๋œ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” + */ + fun fromMaps( + actionMap: Map, LRAction>, + gotoMap: Map, Int>, + terminals: Set, + nonTerminals: Set, + numStates: Int + ): OptimizedParsingTable { + return build { + dimensions(numStates, terminals.size, nonTerminals.size) + terminals(terminals) + nonTerminals(nonTerminals) + actions(actionMap) + gotos(gotoMap) + } + } + } + + /** + * OptimizedParsingTable์„ ์ƒ์„ฑํ•˜๊ธฐ ์œ„ํ•œ DSL ๋นŒ๋” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + class Builder { + private var numStates: Int = 0 + private var numTerminals: Int = 0 + private var numNonTerminals: Int = 0 + private val terminalToIndex = mutableMapOf() + private val nonTerminalToIndex = mutableMapOf() + private val actions = mutableListOf>() + private val gotos = mutableListOf>() + + /** + * ํ…Œ์ด๋ธ”์˜ ์ฐจ์›์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun dimensions(states: Int, terminals: Int, nonTerminals: Int) { + this.numStates = states + this.numTerminals = terminals + this.numNonTerminals = nonTerminals + } + + /** + * ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun terminals(terminals: Set) { + terminals.forEachIndexed { index, terminal -> + terminalToIndex[terminal] = index + } + } + + /** + * ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ๋“ค์„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun nonTerminals(nonTerminals: Set) { + nonTerminals.forEachIndexed { index, nonTerminal -> + nonTerminalToIndex[nonTerminal] = index + } + } + + /** + * ๊ฐœ๋ณ„ ์•ก์…˜์„ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + */ + fun action(state: Int, terminal: TokenType, action: LRAction) { + actions.add(Triple(state, terminal, action)) + } + + /** + * ๊ฐœ๋ณ„ GOTO๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + */ + fun goto(state: Int, nonTerminal: TokenType, nextState: Int) { + gotos.add(Triple(state, nonTerminal, nextState)) + } + + /** + * ์•ก์…˜ ๋งต์„ ์ผ๊ด„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun actions(actionMap: Map, LRAction>) { + for ((key, action) in actionMap) { + action(key.first, key.second, action) + } + } + + /** + * GOTO ๋งต์„ ์ผ๊ด„ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + fun gotos(gotoMap: Map, Int>) { + for ((key, nextState) in gotoMap) { + goto(key.first, key.second, nextState) + } + } + + fun build(): OptimizedParsingTable { + if (numStates <= 0) { + throw ParserException.numStatesNotPositive(numStates) + } + if (numTerminals <= 0) { + throw ParserException.numTerminalsNotPositive(numTerminals) + } + if (numNonTerminals <= 0) { + throw ParserException.numNonTerminalsNotPositive(numNonTerminals) + } + + // 2D ๋ฐฐ์—ด ์ดˆ๊ธฐํ™” + val actionTable2D = Array(numStates) { arrayOfNulls(numTerminals) } + val gotoTable2D = Array(numStates) { IntArray(numNonTerminals) { EMPTY_GOTO_ENTRY } } + + // ์•ก์…˜ ์„ค์ • + for ((state, terminal, action) in actions) { + val terminalIndex = terminalToIndex[terminal] + if (terminalIndex != null && state in 0 until numStates) { + actionTable2D[state][terminalIndex] = action + } + } + + // GOTO ์„ค์ • + for ((state, nonTerminal, nextState) in gotos) { + val nonTerminalIndex = nonTerminalToIndex[nonTerminal] + if (nonTerminalIndex != null && state in 0 until numStates) { + gotoTable2D[state][nonTerminalIndex] = nextState + } + } + + return OptimizedParsingTable( + actionTable2D, + gotoTable2D, + terminalToIndex.toMap(), + nonTerminalToIndex.toMap(), + numStates, + numTerminals, + numNonTerminals + ) + } + } +} + +/** + * ํ…Œ์ด๋ธ” ๋‚ด๋ณด๋‚ด๊ธฐ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class TableExport( + val actionMap: Map, LRAction>, + val gotoMap: Map, Int> +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/ParserService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/ParserService.kt new file mode 100644 index 00000000..8e99f7ee --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/ParserService.kt @@ -0,0 +1,731 @@ +package hs.kr.entrydsm.domain.parser.services + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.ParsingState +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.interfaces.ParserContract +import hs.kr.entrydsm.domain.parser.values.Grammar +import hs.kr.entrydsm.domain.parser.values.ParsingResult +import hs.kr.entrydsm.domain.parser.values.ParsingTable +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType +import hs.kr.entrydsm.global.configuration.ParserConfiguration +import hs.kr.entrydsm.global.configuration.interfaces.ConfigurationProvider +import hs.kr.entrydsm.global.exception.ErrorCode + +/** + * Parser ๋„๋ฉ”์ธ์˜ ํ•ต์‹ฌ ์„œ๋น„์Šค ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Domain Service ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๋ณต์žกํ•œ ํŒŒ์‹ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ , + * ๋‹ค์–‘ํ•œ ํŒŒ์‹ฑ ์ „๋žต๊ณผ ์ตœ์ ํ™” ๊ธฐ๋ฒ•์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. ๋†’์€ ์ˆ˜์ค€์˜ ํŒŒ์‹ฑ ์—ฐ์‚ฐ๊ณผ + * ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ ๊ฐ์ฒด๋“ค ๊ฐ„์˜ ๋ณต์žกํ•œ ์ƒํ˜ธ์ž‘์šฉ์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Service( + name = "ParserService", + type = ServiceType.DOMAIN_SERVICE +) +class ParserService( + private val lrParserTableService: LRParserTableService, + private val firstFollowCalculatorService: FirstFollowCalculatorService, + private val conflictResolverService: ConflictResolverService, + private val configurationProvider: ConfigurationProvider +) : ParserContract { + + private val parsingStatistics = mutableMapOf() + + // ์„ค์ •์€ ConfigurationProvider๋ฅผ ํ†ตํ•ด ๋™์ ์œผ๋กœ ์ ‘๊ทผ + private val config: ParserConfiguration + get() = configurationProvider.getParserConfiguration() + + /** + * ํ† ํฐ ๋ชฉ๋ก์„ ๊ตฌ๋ฌธ ๋ถ„์„ํ•˜์—ฌ AST๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ตฌ๋ฌธ ๋ถ„์„ํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ (AST ๋ฐ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํฌํ•จ) + */ + override fun parse(tokens: List): ParsingResult { + val startTime = System.currentTimeMillis() + + try { + validateTokens(tokens) + updateStatistics(STAT_PARSE_ATTEMPTS, 1) + + val parsingTable = lrParserTableService.buildParsingTable(Grammar) + val result = performLRParsing(tokens, parsingTable) + + val duration = System.currentTimeMillis() - startTime + updateStatistics(STAT_TOTAL_PARSING_TIME, duration) + updateStatistics(STAT_AVERAGE_TOKENS_PER_SECOND, calculateTokensPerSecond(tokens.size, duration)) + + if (result.isSuccess) { + updateStatistics(STAT_SUCCESSFUL_PARSES, 1) + } else { + updateStatistics(STAT_FAILED_PARSES, 1) + } + + return result.copy(duration = duration) + + } catch (e: Exception) { + val duration = System.currentTimeMillis() - startTime + updateStatistics(STAT_ERROR_PARSES, 1) + + return ParsingResult.failure( + error = ParserException( + errorCode = hs.kr.entrydsm.global.exception.ErrorCode.PARSING_ERROR, + message = "ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}", + cause = e + ), + duration = duration, + tokenCount = tokens.size + ) + } + } + + /** + * ๋‹จ์ผ ํ† ํฐ ์ŠคํŠธ๋ฆผ์„ ๊ตฌ๋ฌธ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenSequence ํ† ํฐ ์‹œํ€€์Šค + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + override fun parseSequence(tokenSequence: Sequence): ParsingResult { + val tokens = tokenSequence.take(config.maxTokenCount).toList() + return parse(tokens) + } + + /** + * ์ฃผ์–ด์ง„ ํ† ํฐ ๋ชฉ๋ก์ด ๋ฌธ๋ฒ•์ ์œผ๋กœ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ์œ ํšจํ•˜๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + */ + override fun validate(tokens: List): Boolean { + return try { + val result = parse(tokens) + result.isSuccess && result.ast != null + } catch (e: Exception) { + false + } + } + + /** + * ๋ถ€๋ถ„ ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ตฌ๋ฌธ ์™„์„ฑ, ์—๋Ÿฌ ๋ณต๊ตฌ ๋“ฑ์— ์‚ฌ์šฉ). + * + * @param tokens ๋ถ€๋ถ„ ํ† ํฐ ๋ชฉ๋ก + * @param allowIncomplete ๋ถˆ์™„์ „ํ•œ ๊ตฌ๋ฌธ ํ—ˆ์šฉ ์—ฌ๋ถ€ + * @return ๋ถ€๋ถ„ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + override fun parsePartial(tokens: List, allowIncomplete: Boolean): ParsingResult { + val originalConfig = config + val modifiedConfig = if (allowIncomplete) { + originalConfig.copy(errorRecoveryMode = true) + } else originalConfig + + configurationProvider.updateParserConfiguration(modifiedConfig) + + try { + val result = parse(tokens) + + // ๋ถˆ์™„์ „ํ•œ ํŒŒ์‹ฑ๋„ ์„ฑ๊ณต์œผ๋กœ ์ฒ˜๋ฆฌ (๋ถ€๋ถ„ AST๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ) + if (allowIncomplete && result.isFailure() && result.ast != null) { + return result.copy( + isSuccess = true, + warnings = result.warnings + WARNING_PARTIAL_PARSING + ) + } + + return result + + } finally { + configurationProvider.updateParserConfiguration(originalConfig) + } + } + + /** + * ๋‹ค์Œ์— ์˜ฌ ์ˆ˜ ์žˆ๋Š” ์œ ํšจํ•œ ํ† ํฐ๋“ค์„ ์˜ˆ์ธกํ•ฉ๋‹ˆ๋‹ค. + * + * @param currentTokens ํ˜„์žฌ๊นŒ์ง€์˜ ํ† ํฐ ๋ชฉ๋ก + * @return ๋‹ค์Œ์— ์˜ฌ ์ˆ˜ ์žˆ๋Š” ํ† ํฐ ํƒ€์ž…๋“ค + */ + override fun predictNextTokens(currentTokens: List): Set { + return try { + val parsingTable = lrParserTableService.buildParsingTable(Grammar) + val currentState = determineCurrentState(currentTokens, parsingTable) + + // ํ˜„์žฌ ์ƒํƒœ์—์„œ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์•ก์…˜์˜ ํ„ฐ๋ฏธ๋„๋“ค ๋ฐ˜ํ™˜ + currentState?.actions?.keys?.toSet() ?: emptySet() + + } catch (e: Exception) { + emptySet() + } + } + + /** + * ํŒŒ์‹ฑ ์˜ค๋ฅ˜ ์œ„์น˜์™€ ์˜ˆ์ƒ ํ† ํฐ์„ ๋ถ„์„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๋ถ„์„ํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ์˜ค๋ฅ˜ ๋ถ„์„ ๊ฒฐ๊ณผ + */ + override fun analyzeErrors(tokens: List): Map { + val result = parse(tokens) + + return if (result.isFailure() && result.error != null) { + mapOf( + ERROR_TYPE to ERROR_TYPE_PARSING, + ERROR_MESSAGE to (result.error.message ?: "Unknown parsing error"), + ERROR_TOKEN_COUNT to tokens.size, + ERROR_EXPECTED_TOKENS to predictNextTokens(tokens), + ERROR_POSITION to findErrorPosition(tokens, result.error), + ERROR_SUGGESTIONS to generateErrorSuggestions(tokens, result.error) + ) + } else { + mapOf( + ERROR_TYPE to ERROR_TYPE_NONE, + ERROR_MESSAGE to PARSING_SUCCESS_MESSAGE, + ERROR_TOKEN_COUNT to tokens.size + ) + } + } + + /** + * ํŒŒ์„œ์˜ ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์„œ ์ƒํƒœ ์ •๋ณด + */ + override fun getState(): Map = mapOf( + CONFIG_DEBUG_MODE to config.debugMode, + CONFIG_ERROR_RECOVERY_MODE to config.errorRecoveryMode, + "maxParsingDepth" to config.maxParsingDepth, // ๋ณ„๋„ ์ƒ์ˆ˜ ํ‚ค ์—†์Œ + STATE_PARSING_STATISTICS to parsingStatistics.toMap(), + STATE_GRAMMAR_INFO to Grammar.getGrammarStatistics(), + STATE_IS_READY to true + ) + + /** + * ํŒŒ์„œ๋ฅผ ์ดˆ๊ธฐ ์ƒํƒœ๋กœ ์žฌ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + */ + override fun reset() { + // ์„ค์ •์„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™” + configurationProvider.resetToDefaults() + parsingStatistics.clear() + + // ์„œ๋น„์Šค๋“ค๋„ ๋ฆฌ์…‹ + lrParserTableService.clearCache() + firstFollowCalculatorService.clearCache() + conflictResolverService.reset() + } + + /** + * ํŒŒ์„œ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + override fun getConfiguration(): Map = mapOf( + CONFIG_MAX_PARSING_STEPS to config.maxParsingSteps, + CONFIG_MAX_STACK_DEPTH to config.maxStackDepth, + CONFIG_MAX_TOKEN_COUNT to config.maxTokenCount, + CONFIG_DEBUG_MODE to config.debugMode, + CONFIG_ERROR_RECOVERY_MODE to config.errorRecoveryMode, + CONFIG_ENABLE_OPTIMIZATIONS to config.enableOptimizations, + CONFIG_CACHING_ENABLED to config.cachingEnabled, + CONFIG_STREAMING_BATCH_SIZE to config.streamingBatchSize, + CONFIG_PARSING_STRATEGY to PARSING_STRATEGY_LR1, + CONFIG_OPTIMIZATIONS to OPTIMIZATIONS_LIST + ) + + /** + * ํŒŒ์‹ฑ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต (ํŒŒ์‹ฑ ํšŸ์ˆ˜, ์„ฑ๊ณต๋ฅ , ํ‰๊ท  ์ฒ˜๋ฆฌ ์‹œ๊ฐ„ ๋“ฑ) + */ + override fun getStatistics(): Map { + val totalAttempts = (parsingStatistics[STAT_PARSE_ATTEMPTS] as? Long) ?: 0L + val successfulParses = (parsingStatistics[STAT_SUCCESSFUL_PARSES] as? Long) ?: 0L + val successRate = if (totalAttempts > 0) successfulParses.toDouble() / totalAttempts else 0.0 + + return parsingStatistics.toMap() + mapOf( + STAT_SUCCESS_RATE to successRate, + STAT_TOTAL_ATTEMPTS to totalAttempts, + STAT_GRAMMAR_COMPLEXITY to (Grammar.getGrammarStatistics()["productionCount"] ?: 0), + STAT_AVERAGE_PARSING_TIME to calculateAverageParsingTime() + ) + } + + /** + * ๋””๋ฒ„๊ทธ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ๋””๋ฒ„๊ทธ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + override fun setDebugMode(enabled: Boolean) { + val updatedConfig = config.copy(debugMode = enabled) + configurationProvider.updateParserConfiguration(updatedConfig) + } + + /** + * ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + override fun setErrorRecoveryMode(enabled: Boolean) { + val updatedConfig = config.copy(errorRecoveryMode = enabled) + configurationProvider.updateParserConfiguration(updatedConfig) + } + + /** + * ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxDepth ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด + */ + override fun setMaxParsingDepth(maxDepth: Int) { + if (maxDepth <= 0) { + throw ParserException.maxParsingDepthNotPositive(maxDepth) + } + if (maxDepth > config.maxStackDepth) { + throw ParserException.maxParsingDepthExceedsLimit( + maxDepth = maxDepth, + limit = config.maxStackDepth + ) + } + + val updatedConfig = config.copy(maxParsingDepth = maxDepth) + configurationProvider.updateParserConfiguration(updatedConfig) + } + + /** + * ์ŠคํŠธ๋ฆฌ๋ฐ ๋ชจ๋“œ๋กœ ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ํ† ํฐ ๋ชฉ๋ก + * @param callback ํŒŒ์‹ฑ ์ง„ํ–‰ ์ƒํ™ฉ ์ฝœ๋ฐฑ + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + override fun parseStreaming(tokens: List, callback: (progress: Double) -> Unit): ParsingResult { + val totalSteps = tokens.size * 2 // ๋Œ€๋žต์ ์ธ ์Šคํ… ์ˆ˜ ์ถ”์ • + var currentStep = 0 + + val progressCallback = { + currentStep++ + callback(currentStep.toDouble() / totalSteps) + } + + return performStreamingParsing(tokens, progressCallback) + } + + /** + * ๋น„๋™๊ธฐ์ ์œผ๋กœ ๊ตฌ๋ฌธ ๋ถ„์„์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๋ถ„์„ํ•  ํ† ํฐ ๋ชฉ๋ก + * @param callback ๋ถ„์„ ์™„๋ฃŒ ์‹œ ํ˜ธ์ถœ๋  ์ฝœ๋ฐฑ ํ•จ์ˆ˜ + */ + override fun parseAsync(tokens: List, callback: (ParsingResult) -> Unit) { + // ์‹ค์ œ ๋น„๋™๊ธฐ ๊ตฌํ˜„์€ ์ฝ”๋ฃจํ‹ด์ด๋‚˜ ๋ณ„๋„ ์Šค๋ ˆ๋“œ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•จ + // ์—ฌ๊ธฐ์„œ๋Š” ๋™๊ธฐ์ ์œผ๋กœ ์‹คํ–‰ํ•˜๊ณ  ์ฝœ๋ฐฑ ํ˜ธ์ถœ + val result = parse(tokens) + callback(result) + } + + /** + * ์ฆ๋ถ„ ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * ๊ธฐ์กด ํŒŒ์‹ฑ ๊ฒฐ๊ณผ๋ฅผ ์žฌํ™œ์šฉํ•˜์—ฌ ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @param previousResult ์ด์ „ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + * @param newTokens ์ƒˆ๋กœ์šด ํ† ํฐ ๋ชฉ๋ก + * @param changeStartIndex ๋ณ€๊ฒฝ ์‹œ์ž‘ ์œ„์น˜ + * @return ์ฆ๋ถ„ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + override fun incrementalParse( + previousResult: ParsingResult, + newTokens: List, + changeStartIndex: Int + ): ParsingResult { + // ๊ฐ„๋‹จํ•œ ์ฆ๋ถ„ ํŒŒ์‹ฑ ๊ตฌํ˜„ + // ์‹ค์ œ๋กœ๋Š” ๋” ๋ณต์žกํ•œ ๋กœ์ง์ด ํ•„์š” (ํŒŒ์‹ฑ ํŠธ๋ฆฌ์˜ ๋ถ€๋ถ„ ์žฌ๊ตฌ์„ฑ) + + return if (changeStartIndex == 0 || !previousResult.isSuccess) { + // ์ฒ˜์Œ๋ถ€ํ„ฐ ๋‹ค์‹œ ํŒŒ์‹ฑ + parse(newTokens) + } else { + // ๋ณ€๊ฒฝ ๋ถ€๋ถ„๋งŒ ๋‹ค์‹œ ํŒŒ์‹ฑํ•˜๊ณ  ์ด์ „ ๊ฒฐ๊ณผ์™€ ๋ณ‘ํ•ฉ + // ํ˜„์žฌ๋Š” ๋‹จ์ˆœํžˆ ์ „์ฒด ์žฌํŒŒ์‹ฑ + parse(newTokens).copy( + metadata = previousResult.metadata + (INCREMENTAL_PARSING_FLAG to true) + ) + } + } + + /** + * ๋ฌธ๋ฒ• ๊ทœ์น™์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฌธ๋ฒ•์ด ์œ ํšจํ•˜๋ฉด true + */ + override fun validateGrammar(): Boolean { + return try { + Grammar.isValid() && + lrParserTableService.canBuildParsingTable(Grammar) && + !conflictResolverService.hasUnresolvableConflicts(Grammar) + } catch (e: Exception) { + false + } + } + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ์ถฉ๋Œ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ถฉ๋Œ ์ •๋ณด ๋งต + */ + override fun checkParsingConflicts(): Map { + return try { + val parsingTable = lrParserTableService.buildParsingTable(Grammar) + val conflicts = parsingTable.getConflicts() + + mapOf( + CONFLICT_HAS_CONFLICTS to conflicts.isNotEmpty(), + CONFLICT_TYPES to conflicts.keys, + CONFLICT_COUNT to conflicts.values.sumOf { it.size }, + CONFLICT_CONFLICTS to conflicts, + CONFLICT_RESOLUTION_STRATEGY to conflictResolverService.getResolutionStrategy() + ) + } catch (e: Exception) { + mapOf( + CONFLICT_HAS_CONFLICTS to true, + CONFLICT_ERROR to (e.message ?: "Unknown error") + ) + } + } + + /** + * ํŠน์ • ์œ„์น˜์—์„œ์˜ ํŒŒ์‹ฑ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenIndex ํ† ํฐ ์ธ๋ฑ์Šค + * @return ํŒŒ์‹ฑ ์ปจํ…์ŠคํŠธ ์ •๋ณด + */ + override fun getParsingContext(tokenIndex: Int): Map { + return mapOf( + CONTEXT_TOKEN_INDEX to tokenIndex, + CONTEXT_INFO to CONTEXT_INFO_NOT_IMPLEMENTED, + CONTEXT_AVAILABLE_ACTIONS to emptyList(), + CONTEXT_STACK_DEPTH to 0 + ) + } + + /** + * ํ˜„์žฌ ํŒŒ์‹ฑ ์Šคํƒ์˜ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์‹ฑ ์Šคํƒ ์ •๋ณด + */ + override fun getParsingStack(): List { + // ์‹ค์ œ ํŒŒ์‹ฑ ์ค‘์ด ์•„๋‹ˆ๋ฏ€๋กœ ๋นˆ ์Šคํƒ ๋ฐ˜ํ™˜ + return emptyList() + } + + /** + * ํŒŒ์„œ๊ฐ€ ์ง€์›ํ•˜๋Š” ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜ + */ + override fun getMaxSupportedTokens(): Int = config.maxTokenCount + + /** + * ํŒŒ์„œ์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ •๋ณด + */ + override fun getMemoryUsage(): Map { + val runtime = Runtime.getRuntime() + + return mapOf( + MEMORY_TOTAL to runtime.totalMemory(), + MEMORY_FREE to runtime.freeMemory(), + MEMORY_USED to (runtime.totalMemory() - runtime.freeMemory()), + MEMORY_MAX to runtime.maxMemory(), + MEMORY_PARSING_TABLE_SIZE to estimateParsingTableSize(), + MEMORY_STATISTICS_SIZE to parsingStatistics.size * STATISTICS_MEMORY_MULTIPLIER // ๋Œ€๋žต์  ์ถ”์ • + ) + } + + // Private helper methods + + private fun validateTokens(tokens: List) { + if (tokens.size > config.maxTokenCount) { + throw ParserException.tokenCountExceedsLimit( + count = tokens.size, + limit = config.maxTokenCount + ) + } + } + + private fun performLRParsing(tokens: List, parsingTable: ParsingTable): ParsingResult { + // LR ํŒŒ์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜ ๊ตฌํ˜„ + val stack = mutableListOf() // ์ƒํƒœ ์Šคํƒ + val inputBuffer = tokens.toMutableList() + var currentState = parsingTable.startState + var step = 0 + + stack.add(currentState) + + while (step < config.maxParsingSteps && inputBuffer.isNotEmpty()) { + step++ + + val currentToken = inputBuffer.first() + val action = parsingTable.getAction(currentState, currentToken.type) + + when { + action?.isShift() == true -> { + // Shift ์—ฐ์‚ฐ + inputBuffer.removeAt(0) + currentState = (action as hs.kr.entrydsm.domain.parser.values.LRAction.Shift).state + stack.add(currentState) + } + action?.isReduce() == true -> { + // Reduce ์—ฐ์‚ฐ + val productionId = action.getProductionId() + val production = Grammar.productions.find { it.id == productionId } + ?: throw ParserException( + errorCode = ErrorCode.PARSING_ERROR, + message = "์ƒ์‚ฐ ๊ทœ์น™์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: $productionId" + ) + repeat(production.right.size) { stack.removeLastOrNull() } + + val gotoState = stack.lastOrNull()?.let { + parsingTable.getGoto(it, production.left) + } + + if (gotoState != null) { + currentState = gotoState + stack.add(currentState) + } else { + throw ParserException( + errorCode = ErrorCode.PARSING_ERROR, + message = "Goto ์ƒํƒœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + ) + } + } + action?.isAccept() == true -> { + // Accept + return ParsingResult.success( + ast = createDummyAST(), // ์‹ค์ œ๋กœ๋Š” ์Šคํƒ์—์„œ AST ๊ตฌ์„ฑ + tokenCount = tokens.size, + nodeCount = 1, + maxDepth = 1 + ) + } + else -> { + // Error + if (config.errorRecoveryMode) { + return attemptErrorRecovery(tokens, stack, inputBuffer) + } else { + throw ParserException( + errorCode = ErrorCode.SYNTAX_ERROR, + message = "ํŒŒ์‹ฑ ์˜ค๋ฅ˜: ์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ํ† ํฐ ${currentToken.type}" + ) + } + } + } + } + + throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSING_FAILED, + message = "ํŒŒ์‹ฑ์ด ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค" + ) + } + + private fun performStreamingParsing( + tokens: List, + progressCallback: () -> Unit + ): ParsingResult { + // ์ŠคํŠธ๋ฆฌ๋ฐ ํŒŒ์‹ฑ ๊ตฌํ˜„ (๋‹จ์ˆœํ™”) + val batchSize = config.streamingBatchSize + var processedTokens = 0 + + while (processedTokens < tokens.size) { + val batch = tokens.drop(processedTokens).take(batchSize) + processedTokens += batch.size + progressCallback() + + // ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ + Thread.sleep(STREAMING_SLEEP_TIME) + } + + return parse(tokens) + } + + private fun determineCurrentState(tokens: List, parsingTable: ParsingTable): ParsingState? { + // ํ˜„์žฌ ํ† ํฐ๋“ค๋กœ๋ถ€ํ„ฐ ํŒŒ์‹ฑ ์ƒํƒœ ๊ฒฐ์ • (๋‹จ์ˆœํ™”) + return parsingTable.states[parsingTable.startState] + } + + private fun findErrorPosition(tokens: List, error: ParserException): Int { + // ์˜ค๋ฅ˜ ์œ„์น˜ ์ฐพ๊ธฐ (๋‹จ์ˆœํ™”) + return tokens.size - 1 + } + + private fun generateErrorSuggestions(tokens: List, error: ParserException): List { + return ERROR_SUGGESTIONS_LIST + } + + private fun attemptErrorRecovery( + originalTokens: List, + stack: MutableList, + inputBuffer: MutableList + ): ParsingResult { + // ๊ฐ„๋‹จํ•œ ์—๋Ÿฌ ๋ณต๊ตฌ (ํ† ํฐ ์Šคํ‚ต) + if (inputBuffer.isNotEmpty()) { + inputBuffer.removeAt(0) + return ParsingResult.failure( + error = ParserException( + errorCode = ErrorCode.PARSING_ERROR, + message = "์—๋Ÿฌ ๋ณต๊ตฌ ์ˆ˜ํ–‰๋จ" + ), + partialAST = createDummyAST(), + tokenCount = originalTokens.size + ) + } + + return ParsingResult.failure( + error = ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSING_FAILED, + message = "์—๋Ÿฌ ๋ณต๊ตฌ ์‹คํŒจ" + ), + tokenCount = originalTokens.size + ) + } + + private fun createDummyAST(): hs.kr.entrydsm.domain.ast.entities.NumberNode { + // ์ž„์‹œ AST ๋…ธ๋“œ ์ƒ์„ฑ (NumberNode ์‚ฌ์šฉ) + return hs.kr.entrydsm.domain.ast.entities.NumberNode(0.0) + } + + private fun updateStatistics(key: String, value: Any) { + when (value) { + is Number -> { + val current = parsingStatistics[key] as? Long ?: 0L + parsingStatistics[key] = current + value.toLong() + } + else -> parsingStatistics[key] = value + } + } + + private fun calculateTokensPerSecond(tokenCount: Int, durationMs: Long): Double { + return if (durationMs > 0) (tokenCount * TOKENS_PER_SECOND_MULTIPLIER) / durationMs else 0.0 + } + + private fun calculateAverageParsingTime(): Double { + val totalTime = parsingStatistics[STAT_TOTAL_PARSING_TIME] as? Long ?: 0L + val totalAttempts = parsingStatistics[STAT_PARSE_ATTEMPTS] as? Long ?: 0L + return if (totalAttempts > 0) totalTime.toDouble() / totalAttempts else 0.0 + } + + private fun estimateParsingTableSize(): Long { + return try { + val parsingTable = lrParserTableService.buildParsingTable(Grammar) + // ๋Œ€๋žต์ ์ธ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ถ”์ • + (parsingTable.states.size * PARSING_TABLE_STATE_SIZE + + parsingTable.actionTable.size * PARSING_TABLE_ACTION_SIZE + + parsingTable.gotoTable.size * PARSING_TABLE_GOTO_SIZE) + } catch (e: Exception) { + 0L + } + } + + companion object { + // Statistics keys + private const val STAT_PARSE_ATTEMPTS = "parseAttempts" + private const val STAT_TOTAL_PARSING_TIME = "totalParsingTime" + private const val STAT_AVERAGE_TOKENS_PER_SECOND = "averageTokensPerSecond" + private const val STAT_SUCCESSFUL_PARSES = "successfulParses" + private const val STAT_FAILED_PARSES = "failedParses" + private const val STAT_ERROR_PARSES = "errorParses" + private const val STAT_SUCCESS_RATE = "successRate" + private const val STAT_TOTAL_ATTEMPTS = "totalAttempts" + private const val STAT_GRAMMAR_COMPLEXITY = "grammarComplexity" + private const val STAT_AVERAGE_PARSING_TIME = "averageParsingTime" + + // Configuration keys + private const val CONFIG_MAX_PARSING_STEPS = "maxParsingSteps" + private const val CONFIG_MAX_STACK_DEPTH = "maxStackDepth" + private const val CONFIG_MAX_TOKEN_COUNT = "maxTokenCount" + private const val CONFIG_DEBUG_MODE = "debugMode" + private const val CONFIG_ERROR_RECOVERY_MODE = "errorRecoveryMode" + private const val CONFIG_ENABLE_OPTIMIZATIONS = "enableOptimizations" + private const val CONFIG_CACHING_ENABLED = "cachingEnabled" + private const val CONFIG_STREAMING_BATCH_SIZE = "streamingBatchSize" + private const val CONFIG_PARSING_STRATEGY = "parsingStrategy" + private const val CONFIG_OPTIMIZATIONS = "optimizations" + + // State keys + private const val STATE_PARSING_STATISTICS = "parsingStatistics" + private const val STATE_GRAMMAR_INFO = "grammarInfo" + private const val STATE_IS_READY = "isReady" + + // Error analysis keys + private const val ERROR_TYPE = "errorType" + private const val ERROR_MESSAGE = "message" + private const val ERROR_TOKEN_COUNT = "tokenCount" + private const val ERROR_EXPECTED_TOKENS = "expectedTokens" + private const val ERROR_POSITION = "errorPosition" + private const val ERROR_SUGGESTIONS = "suggestions" + + // Error types + private const val ERROR_TYPE_PARSING = "ParsingError" + private const val ERROR_TYPE_NONE = "None" + + // Conflict check keys + private const val CONFLICT_HAS_CONFLICTS = "hasConflicts" + private const val CONFLICT_TYPES = "conflictTypes" + private const val CONFLICT_COUNT = "conflictCount" + private const val CONFLICT_CONFLICTS = "conflicts" + private const val CONFLICT_RESOLUTION_STRATEGY = "resolutionStrategy" + private const val CONFLICT_ERROR = "error" + + // Parsing context keys + private const val CONTEXT_TOKEN_INDEX = "tokenIndex" + private const val CONTEXT_INFO = "contextInfo" + private const val CONTEXT_AVAILABLE_ACTIONS = "availableActions" + private const val CONTEXT_STACK_DEPTH = "stackDepth" + + // Memory usage keys + private const val MEMORY_TOTAL = "totalMemory" + private const val MEMORY_FREE = "freeMemory" + private const val MEMORY_USED = "usedMemory" + private const val MEMORY_MAX = "maxMemory" + private const val MEMORY_PARSING_TABLE_SIZE = "parsingTableSize" + private const val MEMORY_STATISTICS_SIZE = "statisticsSize" + + // Values + private const val PARSING_STRATEGY_LR1 = "LR(1)" + private const val CONTEXT_INFO_NOT_IMPLEMENTED = "ํŒŒ์‹ฑ ์ปจํ…์ŠคํŠธ ๋ถ„์„ ๋ฏธ๊ตฌํ˜„" + private const val PARSING_SUCCESS_MESSAGE = "ํŒŒ์‹ฑ ์„ฑ๊ณต" + private const val INCREMENTAL_PARSING_FLAG = "incrementalParsing" + + // Optimizations + private val OPTIMIZATIONS_LIST = listOf( + "tableCompression", + "stateMinimization", + "conflictResolution" + ) + + // Error suggestions + private val ERROR_SUGGESTIONS_LIST = listOf( + "๋ฌธ๋ฒ•์„ ํ™•์ธํ•˜์„ธ์š”", + "๊ด„ํ˜ธ๊ฐ€ ๊ท ํ˜•์„ ์ด๋ฃจ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”", + "์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„๋ฅผ ํ™•์ธํ•˜์„ธ์š”" + ) + + // Warnings + private const val WARNING_PARTIAL_PARSING = "๋ถ€๋ถ„ ํŒŒ์‹ฑ ๊ฒฐ๊ณผ์ž…๋‹ˆ๋‹ค" + + // Memory size estimates + private const val STATISTICS_MEMORY_MULTIPLIER = 50L + private const val PARSING_TABLE_STATE_SIZE = 500L + private const val PARSING_TABLE_ACTION_SIZE = 100L + private const val PARSING_TABLE_GOTO_SIZE = 100L + + // Streaming + private const val STREAMING_SLEEP_TIME = 10L + private const val TOKENS_PER_SECOND_MULTIPLIER = 1000.0 + } +} diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/RealLRParserService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/RealLRParserService.kt new file mode 100644 index 00000000..4ded84e3 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/RealLRParserService.kt @@ -0,0 +1,473 @@ +package hs.kr.entrydsm.domain.parser.services + +import hs.kr.entrydsm.global.extensions.* + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.domain.parser.interfaces.GrammarProvider +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.parser.values.ParsingResult +import hs.kr.entrydsm.domain.parser.values.ParsingTable +import hs.kr.entrydsm.domain.parser.values.ParserState +import hs.kr.entrydsm.domain.parser.values.ParsingTraceEntry +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType + +/** + * ์‹ค์ œ LR ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•˜๋Š” ๋„๋ฉ”์ธ ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Domain Service ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ LR(1) ํŒŒ์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์˜ + * ํ•ต์‹ฌ ๊ตฌํ˜„์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. ํ† ํฐ ์ŠคํŠธ๋ฆผ์„ AST๋กœ ๋ณ€ํ™˜ํ•˜๋Š” ๊ณผ์ •์—์„œ + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ํ™œ์šฉํ•œ Shift/Reduce ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•˜๋ฉฐ, + * ์—๋Ÿฌ ๋ณต๊ตฌ์™€ ๋””๋ฒ„๊น… ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Service( + name = "RealLRParserService", + type = ServiceType.DOMAIN_SERVICE +) +class RealLRParserService( + private val grammarProvider: GrammarProvider, + private val parsingTable: ParsingTable +) { + + companion object { + private const val MAX_PARSING_STEPS = 100000 + private const val MAX_STACK_SIZE = 10000 + private const val MAX_ERROR_RECOVERY_ATTEMPTS = 100 + } + + // ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ์บก์Аํ™”ํ•œ ๊ฐ’ ๊ฐ์ฒด + private var parserState = ParserState() + + // ํŒŒ์‹ฑ ์„ค์ • + private var enableErrorRecovery = true + private var enableDebugging = false + private var maxStackSize = MAX_STACK_SIZE + + /** + * ํ† ํฐ ๋ชฉ๋ก์„ LR ํŒŒ์‹ฑํ•˜์—ฌ AST๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ํŒŒ์‹ฑํ•  ํ† ํฐ ๋ชฉ๋ก + * @return ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + */ + fun parse(tokens: List): ParsingResult { + val startTime = System.currentTimeMillis() + + try { + initializeParsing(tokens) + val ast = performLRParsing() + val duration = System.currentTimeMillis() - startTime + + return ParsingResult.success( + ast = ast, + duration = duration, + tokenCount = tokens.size, + nodeCount = calculateNodeCount(ast), + maxDepth = calculateASTDepth(ast), + metadata = createParsingMetadata() + ) + + } catch (e: ParserException) { + val duration = System.currentTimeMillis() - startTime + + return ParsingResult.failure( + error = e, + duration = duration, + tokenCount = tokens.size, + metadata = createParsingMetadata() + ) + + } catch (e: Exception) { + val duration = System.currentTimeMillis() - startTime + + return ParsingResult.failure( + error = ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSING_FAILED, + message = "ํŒŒ์‹ฑ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", + cause = e + ), + duration = duration, + tokenCount = tokens.size, + metadata = createParsingMetadata() + ) + } + } + + /** + * ๋‹จ์ผ ํŒŒ์‹ฑ ๋‹จ๊ณ„๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์‹ฑ์ด ์™„๋ฃŒ๋˜๋ฉด true, ๊ณ„์†ํ•ด์•ผ ํ•˜๋ฉด false + */ + fun parseStep(): Boolean { + if (parserState.currentPosition > parserState.inputTokens.size) { + throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSER_STATE_ERROR, + message = "ํŒŒ์‹ฑ์ด ์ด๋ฏธ ์™„๋ฃŒ๋˜์—ˆ์Šต๋‹ˆ๋‹ค" + ) + } + + val currentToken = getCurrentToken() + val currentState = parserState.stateStack.lastOrNull() ?: throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSER_STATE_ERROR, + message = "์Šคํƒ์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค" + ) + val action = parsingTable.getAction(currentState, currentToken.type) + + if (enableDebugging) { + recordTrace(currentState, currentToken, action) + } + + when { + action == null -> { + if (enableErrorRecovery) { + performErrorRecovery(currentToken) + return false + } else { + throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.UNEXPECTED_EOF, + message = "์˜ˆ์ƒํ•˜์ง€ ๋ชปํ•œ ํ† ํฐ: ${currentToken.type} at position ${parserState.currentPosition}" + ) + } + } + action.isShift() -> { + performShift(action, currentToken) + return false + } + action.isReduce() -> { + performReduce(action) + return false + } + action.isAccept() -> { + return true // ํŒŒ์‹ฑ ์™„๋ฃŒ + } + else -> { + throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSER_STATE_ERROR, + message = "์•Œ ์ˆ˜ ์—†๋Š” ์•ก์…˜: $action" + ) + } + } + } + + /** + * ํ˜„์žฌ ํŒŒ์‹ฑ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์‹ฑ ์ƒํƒœ ์ •๋ณด + */ + fun getCurrentState(): Map = mapOf( + "currentPosition" to parserState.currentPosition, + "inputSize" to parserState.inputTokens.size, + "stackSize" to parserState.stateStack.size, + "currentStateId" to (parserState.stateStack.lastOrNull() ?: -1), + "currentToken" to (getCurrentToken().type.name), + "parsingSteps" to parserState.parsingSteps, + "shiftOperations" to parserState.shiftOperations, + "reduceOperations" to parserState.reduceOperations, + "errorRecoveryAttempts" to parserState.errorRecoveryAttempts + ) + + /** + * ํŒŒ์‹ฑ ์ถ”์  ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์‹ฑ ์ถ”์  ๋ชฉ๋ก + */ + fun getParsingTrace(): List = parserState.parsingTrace.toList() + + /** + * ํŒŒ์„œ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun reset() { + parserState.stateStack.clear() + parserState.astStack.clear() + parserState.inputTokens.clear() + parserState.currentPosition = 0 + parserState.parsingSteps = 0 + parserState.shiftOperations = 0 + parserState.reduceOperations = 0 + parserState.errorRecoveryAttempts = 0 + parserState.parsingTrace.clear() + } + + /** + * ์—๋Ÿฌ ๋ณต๊ตฌ ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ์—๋Ÿฌ ๋ณต๊ตฌ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + fun setErrorRecoveryEnabled(enabled: Boolean) { + this.enableErrorRecovery = enabled + } + + /** + * ๋””๋ฒ„๊น… ๋ชจ๋“œ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param enabled ๋””๋ฒ„๊น… ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + */ + fun setDebuggingEnabled(enabled: Boolean) { + this.enableDebugging = enabled + } + + /** + * ์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxSize ์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ + */ + fun setMaxStackSize(maxSize: Int) { + if (maxSize <= 0) { + throw ParserException.maxStackSizeNotPositive(maxSize) + } + this.maxStackSize = maxSize + } + + /** + * ํŒŒ์‹ฑ์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun initializeParsing(tokens: List) { + reset() + parserState.inputTokens.addAll(tokens) + parserState.inputTokens.add(Token(TokenType.DOLLAR, "$", hs.kr.entrydsm.global.values.Position.of(0))) // EOF ํ† ํฐ ์ถ”๊ฐ€ + parserState.stateStack.add(parsingTable.startState) + parserState.currentPosition = 0 + parserState.parsingSteps = 0 + } + + /** + * LR ํŒŒ์‹ฑ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performLRParsing(): ASTNode { + while (parserState.parsingSteps < MAX_PARSING_STEPS) { + if (parserState.stateStack.size > maxStackSize) { + throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSER_STATE_ERROR, + message = "์Šคํƒ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ: ${parserState.stateStack.size} > $maxStackSize" + ) + } + + if (parseStep()) { + // ํŒŒ์‹ฑ ์™„๋ฃŒ + return parserState.astStack.lastOrNull() ?: createEmptyAST() + } + + parserState.parsingSteps++ + } + + throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSING_FAILED, + message = "ํŒŒ์‹ฑ์ด ์ตœ๋Œ€ ๋‹จ๊ณ„ ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค: $MAX_PARSING_STEPS" + ) + } + + /** + * Shift ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performShift(action: LRAction, token: Token) { + val nextState = (action as hs.kr.entrydsm.domain.parser.values.LRAction.Shift).state + parserState.stateStack.add(nextState) + parserState.astStack.add(createLeafNode(token)) + parserState.currentPosition++ + parserState.shiftOperations++ + + if (enableDebugging) { + println("SHIFT: state ${parserState.stateStack[parserState.stateStack.size - 2]} -> $nextState, token: ${token.type}") + } + } + + /** + * Reduce ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performReduce(action: LRAction) { + val productionId = action.getProductionId() + val production = grammarProvider.getProductionById(productionId) + + // ์Šคํƒ์—์„œ ์‹ฌ๋ณผ๋“ค ์ œ๊ฑฐ + val children = mutableListOf() + repeat(production.right.size) { + if (parserState.stateStack.isNotEmpty()) parserState.stateStack.removeLastOrNull() + children.add(0, parserState.astStack.removeLastOrNull()) // ์—ญ์ˆœ์œผ๋กœ ์ถ”๊ฐ€ + } + + // AST ๋…ธ๋“œ ์ƒ์„ฑ + val astNode = production.buildAST(children.filterNotNull() as List) as? hs.kr.entrydsm.domain.ast.entities.ASTNode + parserState.astStack.add(astNode) + + // Goto ์—ฐ์‚ฐ + val currentState = parserState.stateStack.lastOrNull() ?: throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSER_STATE_ERROR, + message = "Reduce ํ›„ ์Šคํƒ์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค" + ) + val gotoState = parsingTable.getGoto(currentState, production.left) + ?: throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSER_STATE_ERROR, + message = "Goto ์ƒํƒœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค: state $currentState, symbol ${production.left}" + ) + + parserState.stateStack.add(gotoState) + parserState.reduceOperations++ + + if (enableDebugging) { + println("REDUCE: production $productionId (${production.left} -> ${production.right.joinToString(" ")})") + println(" goto state $gotoState") + } + } + + /** + * ์—๋Ÿฌ ๋ณต๊ตฌ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun performErrorRecovery(currentToken: Token) { + parserState.errorRecoveryAttempts++ + + if (parserState.errorRecoveryAttempts > MAX_ERROR_RECOVERY_ATTEMPTS) { + throw ParserException( + errorCode = hs.kr.entrydsm.global.constants.ErrorCodes.Parser.PARSING_FAILED, + message = "์—๋Ÿฌ ๋ณต๊ตฌ ์‹œ๋„ ํšŸ์ˆ˜ ์ดˆ๊ณผ: $MAX_ERROR_RECOVERY_ATTEMPTS" + ) + } + + // ๊ฐ„๋‹จํ•œ ์—๋Ÿฌ ๋ณต๊ตฌ: ํ˜„์žฌ ํ† ํฐ ์Šคํ‚ต + parserState.currentPosition++ + + if (enableDebugging) { + println("ERROR RECOVERY: skipping token ${currentToken.type} at position ${parserState.currentPosition - 1}") + } + } + + /** + * ํ˜„์žฌ ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun getCurrentToken(): Token { + return if (parserState.currentPosition < parserState.inputTokens.size) { + parserState.inputTokens[parserState.currentPosition] + } else { + parserState.inputTokens.last() // EOF ํ† ํฐ + } + } + + /** + * ๋ฆฌํ”„ AST ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private fun createLeafNode(token: Token): hs.kr.entrydsm.domain.ast.entities.NumberNode { + return hs.kr.entrydsm.domain.ast.entities.NumberNode( + if (token.type == hs.kr.entrydsm.domain.lexer.entities.TokenType.NUMBER) { + token.value.toDoubleOrNull() ?: 0.0 + } else { + 0.0 // ๊ธฐ๋ณธ๊ฐ’ + } + ) + } + + /** + * ๋นˆ AST ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private fun createEmptyAST(): hs.kr.entrydsm.domain.ast.entities.NumberNode { + return hs.kr.entrydsm.domain.ast.entities.NumberNode(0.0) + } + + /** + * AST์˜ ๋…ธ๋“œ ๊ฐœ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun calculateNodeCount(ast: ASTNode): Int { + return 1 + ast.getChildren().sumOf { calculateNodeCount(it) } + } + + /** + * AST์˜ ๊นŠ์ด๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun calculateASTDepth(ast: ASTNode): Int { + return if (ast.getChildren().isEmpty()) { + 1 + } else { + 1 + (ast.getChildren().maxOfOrNull { calculateASTDepth(it) } ?: 0) + } + } + + /** + * ํŒŒ์‹ฑ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + private fun createParsingMetadata(): Map = mapOf( + "parsingSteps" to parserState.parsingSteps, + "shiftOperations" to parserState.shiftOperations, + "reduceOperations" to parserState.reduceOperations, + "errorRecoveryAttempts" to parserState.errorRecoveryAttempts, + "maxStackDepth" to parserState.stateStack.size, + "finalPosition" to parserState.currentPosition, + "parsingTraceSize" to parserState.parsingTrace.size, + "enableErrorRecovery" to enableErrorRecovery, + "enableDebugging" to enableDebugging + ) + + /** + * ํŒŒ์‹ฑ ์ถ”์  ์ •๋ณด๋ฅผ ๊ธฐ๋กํ•ฉ๋‹ˆ๋‹ค. + */ + private fun recordTrace(state: Int, token: Token, action: LRAction?) { + val actionStr = action?.getActionType() ?: "ERROR" + val traceEntry = if (action?.isShift() == true) { + ParsingTraceEntry.shift(state, token, parserState.stateStack.lastOrNull() ?: 0, parserState.parsingSteps) + } else { + ParsingTraceEntry( + step = parserState.parsingSteps, + action = actionStr, + state = state, + token = token, + production = null, + stackSnapshot = parserState.stateStack.toList() + ) + } + parserState.parsingTrace.add(traceEntry) + } + + + /** + * ์„œ๋น„์Šค์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getConfiguration(): Map = mapOf( + "maxParsingSteps" to MAX_PARSING_STEPS, + "maxStackSize" to maxStackSize, + "maxErrorRecoveryAttempts" to MAX_ERROR_RECOVERY_ATTEMPTS, + "enableErrorRecovery" to enableErrorRecovery, + "enableDebugging" to enableDebugging, + "grammarComplexity" to grammarProvider.calculateComplexity(), + "parsingTableSize" to parsingTable.getSizeInfo() + ) + + /** + * ์„œ๋น„์Šค ์‚ฌ์šฉ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + "serviceName" to "RealLRParserService", + "currentSessionStats" to getCurrentState(), + "totalTraceEntries" to parserState.parsingTrace.size, + "operationDistribution" to mapOf( + "shift" to parserState.shiftOperations, + "reduce" to parserState.reduceOperations, + "errorRecovery" to parserState.errorRecoveryAttempts + ) + ) + + /** + * ํŒŒ์‹ฑ ์ถ”์ ์„ ๋ฌธ์ž์—ด๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒŒ์‹ฑ ์ถ”์  ๋ฌธ์ž์—ด + */ + fun dumpParsingTrace(): String = buildString { + appendLine("=== LR ํŒŒ์‹ฑ ์ถ”์  ์ •๋ณด ===") + appendLine("์ด ๋‹จ๊ณ„: ${parserState.parsingSteps}") + appendLine("Shift ์—ฐ์‚ฐ: ${parserState.shiftOperations}") + appendLine("Reduce ์—ฐ์‚ฐ: ${parserState.reduceOperations}") + appendLine("์—๋Ÿฌ ๋ณต๊ตฌ: ${parserState.errorRecoveryAttempts}") + appendLine() + + parserState.parsingTrace.forEach { entry -> + appendLine(entry.toString()) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/StateCacheManager.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/StateCacheManager.kt new file mode 100644 index 00000000..c1cd4ea1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/services/StateCacheManager.kt @@ -0,0 +1,384 @@ +package hs.kr.entrydsm.domain.parser.services + +import hs.kr.entrydsm.domain.parser.entities.CompressedLRState +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.global.annotation.service.Service +import hs.kr.entrydsm.global.annotation.service.type.ServiceType +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.atomic.AtomicLong + +/** + * LR ํŒŒ์„œ์˜ ์ƒํƒœ ์บ์‹ฑ ๋ฐ ๋ฉ”๋ชจ๋ฆฌ ์ตœ์ ํ™”๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์„œ๋น„์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋™์ผํ•œ ์ƒํƒœ๋ฅผ ์žฌ์‚ฌ์šฉํ•˜์—ฌ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ค„์ด๊ณ  ์„ฑ๋Šฅ์„ ํ–ฅ์ƒ์‹œํ‚ค๋ฉฐ, + * LALR ์ƒํƒœ ๋ณ‘ํ•ฉ๊ณผ ์••์ถ•์„ ํ†ตํ•ด ํŒŒ์„œ ํ…Œ์ด๋ธ”์˜ ํฌ๊ธฐ๋ฅผ ์ตœ์ ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ ์ƒํƒœ ์บ์‹ฑ ์‹œ์Šคํ…œ์„ DDD ๊ตฌ์กฐ๋กœ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +@Service( + name = "StateCacheManager", + type = ServiceType.DOMAIN_SERVICE +) +class StateCacheManager { + + // ์ƒํƒœ ์บ์‹œ: ์ƒํƒœ ์ง‘ํ•ฉ -> ์ƒํƒœ ID ๋งคํ•‘ + private val stateCache = ConcurrentHashMap, Int>() + private val reverseStateCache = ConcurrentHashMap>() // ์—ญ๋ฐฉํ–ฅ ์กฐํšŒ์šฉ + + // ์••์ถ•๋œ ์ƒํƒœ ์บ์‹œ: ์‹œ๊ทธ๋‹ˆ์ฒ˜ -> ์ƒํƒœ ID ๋งคํ•‘ + private val compressedStateCache = ConcurrentHashMap() + private val reverseCompressedStateCache = ConcurrentHashMap() // ์—ญ๋ฐฉํ–ฅ ์กฐํšŒ์šฉ + + // ์ƒํƒœ๋ณ„ ์ฐธ์กฐ ์นด์šดํŠธ + private val referenceCount = ConcurrentHashMap() + + // ์ƒํƒœ ์ƒ์„ฑ ํ†ต๊ณ„ + private val creationStats = CacheStatistics() + + // ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ถ”์  + private val memoryTracker = MemoryTracker() + + /** + * ์ฃผ์–ด์ง„ ์ƒํƒœ ์ง‘ํ•ฉ์— ๋Œ€ํ•ด ์บ์‹œ๋œ ์ƒํƒœ ID๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param state ์ƒํƒœ ์ง‘ํ•ฉ + * @param stateId ์ƒˆ๋กœ ํ• ๋‹นํ•  ์ƒํƒœ ID (์บ์‹œ ๋ฏธ์Šค์ธ ๊ฒฝ์šฐ) + * @return ์บ์‹œ๋œ ์ƒํƒœ ID ๋˜๋Š” ์ƒˆ๋กœ ๋“ฑ๋ก๋œ ์ƒํƒœ ID + */ + fun getOrCacheState(state: Set, stateId: Int): CacheResult { + val existingId = stateCache[state] + + return if (existingId != null) { + // ์บ์‹œ ํžˆํŠธ + incrementReference(existingId) + creationStats.recordHit() + CacheResult.hit(existingId) + } else { + // ์บ์‹œ ๋ฏธ์Šค - ์ƒˆ ์ƒํƒœ ๋“ฑ๋ก + stateCache[state] = stateId + reverseStateCache[stateId] = state // ์—ญ๋ฐฉํ–ฅ ์บ์‹œ ์ถ”๊ฐ€ + referenceCount[stateId] = AtomicLong(1) + memoryTracker.recordStateCreation(state) + creationStats.recordMiss() + CacheResult.miss(stateId) + } + } + + /** + * ์••์ถ•๋œ ์ƒํƒœ์— ๋Œ€ํ•ด ์บ์‹œ๋œ ์ƒํƒœ ID๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ์ƒˆ๋กœ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param compressedState ์••์ถ•๋œ ์ƒํƒœ + * @param stateId ์ƒˆ๋กœ ํ• ๋‹นํ•  ์ƒํƒœ ID (์บ์‹œ ๋ฏธ์Šค์ธ ๊ฒฝ์šฐ) + * @return ์บ์‹œ๋œ ์ƒํƒœ ID ๋˜๋Š” ์ƒˆ๋กœ ๋“ฑ๋ก๋œ ์ƒํƒœ ID + */ + fun getOrCacheCompressedState( + compressedState: CompressedLRState, + stateId: Int + ): CacheResult { + val signature = compressedState.signature + val existingId = compressedStateCache[signature] + + return if (existingId != null) { + // ์บ์‹œ ํžˆํŠธ + incrementReference(existingId) + creationStats.recordCompressedHit() + CacheResult.hit(existingId) + } else { + // ์บ์‹œ ๋ฏธ์Šค - ์ƒˆ ์ƒํƒœ ๋“ฑ๋ก + compressedStateCache[signature] = stateId + reverseCompressedStateCache[stateId] = signature // ์—ญ๋ฐฉํ–ฅ ์บ์‹œ ์ถ”๊ฐ€ + referenceCount[stateId] = AtomicLong(1) + memoryTracker.recordCompressedStateCreation(compressedState) + creationStats.recordCompressedMiss() + CacheResult.miss(stateId) + } + } + + /** + * LALR ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param newState ์ƒˆ๋กœ์šด ์ƒํƒœ + * @return ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ ID ๋˜๋Š” null + */ + fun findMergeableState(newState: CompressedLRState): Int? { + val signature = newState.signature + + for ((cachedSignature, stateId) in compressedStateCache) { + if (cachedSignature != signature) continue + + // ๋™์ผํ•œ ์‹œ๊ทธ๋‹ˆ์ฒ˜๋ฅผ ๊ฐ€์ง„ ์ƒํƒœ๋ฅผ ์ฐพ์•˜์œผ๋ฏ€๋กœ ๋ณ‘ํ•ฉ ๊ฐ€๋Šฅ์„ฑ ํ™•์ธ + // ์‹ค์ œ๋กœ๋Š” ๋” ์ •๊ตํ•œ LALR ๋ณ‘ํ•ฉ ์กฐ๊ฑด ๊ฒ€์‚ฌ๊ฐ€ ํ•„์š” + return stateId + } + + return null + } + + /** + * ์ƒํƒœ์˜ ์ฐธ์กฐ ์นด์šดํŠธ๋ฅผ ์ฆ๊ฐ€์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @param stateId ์ƒํƒœ ID + */ + fun incrementReference(stateId: Int) { + referenceCount.computeIfAbsent(stateId) { AtomicLong(0) }.incrementAndGet() + } + + /** + * ์ƒํƒœ์˜ ์ฐธ์กฐ ์นด์šดํŠธ๋ฅผ ๊ฐ์†Œ์‹œํ‚ต๋‹ˆ๋‹ค. + * + * @param stateId ์ƒํƒœ ID + * @return ๊ฐ์†Œ ํ›„ ์ฐธ์กฐ ์นด์šดํŠธ + */ + fun decrementReference(stateId: Int): Long { + val counter = referenceCount[stateId] ?: return 0 + val newCount = counter.decrementAndGet() + + if (newCount <= 0) { + // ์ฐธ์กฐ๊ฐ€ ์—†์œผ๋ฉด ์ •๋ฆฌ ๋Œ€์ƒ์œผ๋กœ ๋งˆํ‚น + markForCleanup(stateId) + } + + return newCount + } + + /** + * ํŠน์ • ์ƒํƒœ๋ฅผ ์ •๋ฆฌ ๋Œ€์ƒ์œผ๋กœ ๋งˆํ‚นํ•ฉ๋‹ˆ๋‹ค. + * + * @param stateId ์ •๋ฆฌํ•  ์ƒํƒœ ID + */ + private fun markForCleanup(stateId: Int) { + // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ์ง€์—ฐ ์ •๋ฆฌ ํ์— ์ถ”๊ฐ€ํ•˜๊ฑฐ๋‚˜ + // ์ฃผ๊ธฐ์ ์ธ ๊ฐ€๋น„์ง€ ์ปฌ๋ ‰์…˜์„ ํ†ตํ•ด ์ •๋ฆฌ + memoryTracker.recordStateCleanup(stateId) + } + + /** + * ๋ฉ”๋ชจ๋ฆฌ ์••๋ฐ• ์‹œ ์บ์‹œ๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param memoryPressureLevel ๋ฉ”๋ชจ๋ฆฌ ์••๋ฐ• ์ˆ˜์ค€ (0.0 ~ 1.0) + * @return ์ •๋ฆฌ๋œ ์ƒํƒœ ๊ฐœ์ˆ˜ + */ + fun performMemoryCleanup(memoryPressureLevel: Double = 0.8): Int { + if (memoryPressureLevel < 0.5) { + return 0 // ๋ฉ”๋ชจ๋ฆฌ ์••๋ฐ•์ด ์‹ฌํ•˜์ง€ ์•Š์Œ + } + + var cleanedCount = 0 + val lowReferenceStates = mutableListOf() + + // ์ฐธ์กฐ ์นด์šดํŠธ๊ฐ€ ๋‚ฎ์€ ์ƒํƒœ๋“ค์„ ์ฐพ์•„์„œ ์ •๋ฆฌ + for ((stateId, counter) in referenceCount) { + if (counter.get() <= 1) { + lowReferenceStates.add(stateId) + } + } + + // ์ •๋ฆฌ ์ˆ˜ํ–‰ + for (stateId in lowReferenceStates.take((lowReferenceStates.size * memoryPressureLevel).toInt())) { + cleanupState(stateId) + cleanedCount++ + } + + creationStats.recordCleanup(cleanedCount) + return cleanedCount + } + + /** + * ํŠน์ • ์ƒํƒœ๋ฅผ ์บ์‹œ์—์„œ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param stateId ์ œ๊ฑฐํ•  ์ƒํƒœ ID + */ + private fun cleanupState(stateId: Int) { + // ์—ญ๋ฐฉํ–ฅ ์บ์‹œ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ O(1) ์‹œ๊ฐ„ ๋ณต์žก๋„๋กœ ์ œ๊ฑฐ + reverseStateCache.remove(stateId)?.let { stateCache.remove(it) } + reverseCompressedStateCache.remove(stateId)?.let { compressedStateCache.remove(it) } + + referenceCount.remove(stateId) + memoryTracker.recordStateCleanup(stateId) + } + + /** + * ์บ์‹œ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์บ์‹œ ํ†ต๊ณ„ ์ •๋ณด + */ + fun getCacheStatistics(): Map { + val hitRate = creationStats.getHitRate() + val compressedHitRate = creationStats.getCompressedHitRate() + + return mapOf( + "totalStates" to stateCache.size, + "compressedStates" to compressedStateCache.size, + "totalReferences" to referenceCount.values.sumOf { it.get() }, + "hitRate" to hitRate, + "compressedHitRate" to compressedHitRate, + "overallHitRate" to (hitRate + compressedHitRate) / 2, + "memoryStats" to memoryTracker.getMemoryStatistics(), + "cacheEfficiency" to calculateCacheEfficiency() + ) + } + + /** + * ์บ์‹œ ํšจ์œจ์„ฑ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์บ์‹œ ํšจ์œจ์„ฑ (0.0 ~ 1.0) + */ + private fun calculateCacheEfficiency(): Double { + val totalRequests = creationStats.hits + creationStats.misses + if (totalRequests == 0L) return 0.0 + + val hitRate = creationStats.hits.toDouble() / totalRequests + val memoryEfficiency = 1.0 - (stateCache.size.toDouble() / maxOf(1, totalRequests)) + + return (hitRate + memoryEfficiency) / 2 + } + + /** + * ์บ์‹œ๋ฅผ ์™„์ „ํžˆ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun clearCache() { + stateCache.clear() + reverseStateCache.clear() + compressedStateCache.clear() + reverseCompressedStateCache.clear() + referenceCount.clear() + creationStats.reset() + memoryTracker.reset() + } + + /** + * ์บ์‹œ ์ƒํƒœ ๋ณด๊ณ ์„œ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธํ•œ ์บ์‹œ ๋ณด๊ณ ์„œ + */ + fun generateCacheReport(): String { + val stats = getCacheStatistics() + val sb = StringBuilder() + + sb.appendLine("=== ์ƒํƒœ ์บ์‹œ ๊ด€๋ฆฌ ๋ณด๊ณ ์„œ ===") + sb.appendLine("์ด ์ƒํƒœ ์ˆ˜: ${stats["totalStates"]}") + sb.appendLine("์••์ถ•๋œ ์ƒํƒœ ์ˆ˜: ${stats["compressedStates"]}") + sb.appendLine("์ด ์ฐธ์กฐ ์ˆ˜: ${stats["totalReferences"]}") + sb.appendLine("ํžˆํŠธ์œจ: ${String.format("%.2f%%", (stats["hitRate"] as Double) * 100)}") + sb.appendLine("์••์ถ• ํžˆํŠธ์œจ: ${String.format("%.2f%%", (stats["compressedHitRate"] as Double) * 100)}") + sb.appendLine("์ „์ฒด ํžˆํŠธ์œจ: ${String.format("%.2f%%", (stats["overallHitRate"] as Double) * 100)}") + sb.appendLine("์บ์‹œ ํšจ์œจ์„ฑ: ${String.format("%.2f%%", (stats["cacheEfficiency"] as Double) * 100)}") + sb.appendLine() + + @Suppress("UNCHECKED_CAST") + val memoryStats = stats["memoryStats"] as Map + sb.appendLine("=== ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ===") + sb.appendLine("์ถ”์ • ๋ฉ”๋ชจ๋ฆฌ: ${memoryStats["estimatedMemoryBytes"]} bytes") + sb.appendLine("ํ‰๊ท  ์ƒํƒœ ํฌ๊ธฐ: ${memoryStats["averageStateSize"]} items") + sb.appendLine("๋ฉ”๋ชจ๋ฆฌ ํšจ์œจ์„ฑ: ${memoryStats["memoryEfficiency"]}") + + return sb.toString() + } + + companion object { + /** + * ์‹ฑ๊ธ€ํ†ค ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + */ + fun create(): StateCacheManager = StateCacheManager() + } +} + +/** + * ์บ์‹œ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class CacheResult( + val stateId: Int, + val isHit: Boolean +) { + companion object { + fun hit(stateId: Int): CacheResult = CacheResult(stateId, true) + fun miss(stateId: Int): CacheResult = CacheResult(stateId, false) + } +} + +/** + * ์บ์‹œ ํ†ต๊ณ„๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +private class CacheStatistics { + var hits: Long = 0 + var misses: Long = 0 + var compressedHits: Long = 0 + var compressedMisses: Long = 0 + var cleanups: Long = 0 + + fun recordHit() { hits++ } + fun recordMiss() { misses++ } + fun recordCompressedHit() { compressedHits++ } + fun recordCompressedMiss() { compressedMisses++ } + fun recordCleanup(count: Int) { cleanups += count } + + fun getHitRate(): Double { + val total = hits + misses + return if (total > 0) hits.toDouble() / total else 0.0 + } + + fun getCompressedHitRate(): Double { + val total = compressedHits + compressedMisses + return if (total > 0) compressedHits.toDouble() / total else 0.0 + } + + fun reset() { + hits = 0 + misses = 0 + compressedHits = 0 + compressedMisses = 0 + cleanups = 0 + } +} + +/** + * ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ถ”์ ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +private class MemoryTracker { + private var totalStatesCreated: Long = 0 + private var totalItemsCreated: Long = 0 + private var totalStatesDestroyed: Long = 0 + + fun recordStateCreation(state: Set) { + totalStatesCreated++ + totalItemsCreated += state.size + } + + fun recordCompressedStateCreation(state: CompressedLRState) { + totalStatesCreated++ + totalItemsCreated += state.getCoreItemCount() + } + + fun recordStateCleanup(stateId: Int) { + totalStatesDestroyed++ + } + + fun getMemoryStatistics(): Map { + val currentStates = totalStatesCreated - totalStatesDestroyed + val averageStateSize = if (totalStatesCreated > 0) { + totalItemsCreated.toDouble() / totalStatesCreated + } else 0.0 + + return mapOf( + "totalStatesCreated" to totalStatesCreated, + "totalStatesDestroyed" to totalStatesDestroyed, + "currentStates" to currentStates, + "totalItemsCreated" to totalItemsCreated, + "averageStateSize" to averageStateSize, + "estimatedMemoryBytes" to (currentStates * averageStateSize * 64), // ๋Œ€๋žต์ ์ธ ์ถ”์ • + "memoryEfficiency" to if (totalStatesCreated > 0) { + (totalStatesCreated - totalStatesDestroyed).toDouble() / totalStatesCreated + } else 1.0 + ) + } + + fun reset() { + totalStatesCreated = 0 + totalItemsCreated = 0 + totalStatesDestroyed = 0 + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/GrammarConsistencySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/GrammarConsistencySpec.kt new file mode 100644 index 00000000..cf2b7159 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/GrammarConsistencySpec.kt @@ -0,0 +1,582 @@ +package hs.kr.entrydsm.domain.parser.specifications + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.values.Grammar +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority +import kotlin.collections.flatMap +import kotlin.collections.map + +/** + * ๋ฌธ๋ฒ•์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” Specification ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Specification ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๋ฌธ๋ฒ• ๊ทœ์น™๋“ค ๊ฐ„์˜ ์ผ๊ด€์„ฑ๊ณผ + * ๋…ผ๋ฆฌ์  ๋ฌด๊ฒฐ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์บก์Аํ™”ํ•ฉ๋‹ˆ๋‹ค. + * ๋ฌธ๋ฒ•์˜ ๊ตฌ์กฐ์  ์ผ๊ด€์„ฑ, ์˜๋ฏธ์  ์ผ๊ด€์„ฑ, ํŒŒ์‹ฑ ๊ฐ€๋Šฅ์„ฑ์„ ์ข…ํ•ฉ์ ์œผ๋กœ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Specification( + name = "GrammarConsistency", + description = "๋ฌธ๋ฒ• ๊ทœ์น™๋“ค ๊ฐ„์˜ ๊ตฌ์กฐ์  ๋ฐ ์˜๋ฏธ์  ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ", + domain = "parser", + priority = Priority.HIGH +) +class GrammarConsistencySpec { + + companion object { + private const val MAX_RECURSION_DEPTH = 1000 + private const val MAX_DERIVATION_STEPS = 10000 + private const val MAX_SYMBOL_DEPENDENCIES = 500 + } + + /** + * ๋ฌธ๋ฒ• ์ „์ฒด์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param grammar ๊ฒ€์ฆํ•  ๋ฌธ๋ฒ• + * @return ์ผ๊ด€์„ฑ์ด ์žˆ์œผ๋ฉด true + */ + fun isSatisfiedBy(grammar: Grammar): Boolean { + return hasStructuralConsistency(grammar) && + hasSemanticConsistency(grammar) && + hasParsingConsistency(grammar) && + hasTerminalConsistency(grammar) && + hasNonTerminalConsistency(grammar) && + hasProductionConsistency(grammar) && + hasStartSymbolConsistency(grammar) + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™๋“ค์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ๊ฒ€์ฆํ•  ์ƒ์‚ฐ ๊ทœ์น™๋“ค + * @param terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @return ์ผ๊ด€์„ฑ์ด ์žˆ์œผ๋ฉด true + */ + fun isProductionSetConsistent( + productions: List, + terminals: Set, + nonTerminals: Set + ): Boolean { + return hasValidSymbolUsage(productions, terminals, nonTerminals) && + hasNoDuplicateProductions(productions) && + hasValidProductionStructure(productions) && + hasConsistentASTBuilders(productions) + } + + /** + * ์‹ฌ๋ณผ ์˜์กด์„ฑ์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ์ƒ์‚ฐ ๊ทœ์น™๋“ค + * @return ์˜์กด์„ฑ์ด ์ผ๊ด€์„ฑ ์žˆ์œผ๋ฉด true + */ + fun hasConsistentDependencies(productions: List): Boolean { + return hasNoCyclicDependencies(productions) && + hasValidDependencyStructure(productions) && + hasReachableDependencies(productions) && + hasFiniteDependencies(productions) + } + + /** + * ๊ตฌ์กฐ์  ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasStructuralConsistency(grammar: Grammar): Boolean { + // ์‹œ์ž‘ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ธ์ง€ ํ™•์ธ + if (!grammar.isNonTerminal(grammar.startSymbol)) { + return false + } + + // ํ„ฐ๋ฏธ๋„๊ณผ ๋…ผํ„ฐ๋ฏธ๋„์˜ ๊ต์ง‘ํ•ฉ์ด ์—†๋Š”์ง€ ํ™•์ธ + if (grammar.terminals.intersect(grammar.nonTerminals).isNotEmpty()) { + return false + } + + // ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™์˜ ์ขŒ๋ณ€์ด ๋…ผํ„ฐ๋ฏธ๋„์ธ์ง€ ํ™•์ธ + if (grammar.productions.any { !grammar.isNonTerminal(it.left) }) { + return false + } + + // ์‹œ์ž‘ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ ์ƒ์‚ฐ ๊ทœ์น™์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + if (grammar.getProductionsFor(grammar.startSymbol).isEmpty()) { + return false + } + + return true + } + + /** + * ์˜๋ฏธ์  ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasSemanticConsistency(grammar: Grammar): Boolean { + // ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„์ด ์ตœ์ข…์ ์œผ๋กœ ํ„ฐ๋ฏธ๋„๋กœ ์œ ๋„๋  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธ + val productiveSymbols = findProductiveSymbols(grammar.productions, grammar.terminals) + if (!grammar.nonTerminals.all { it in productiveSymbols }) { + return false + } + + // ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„์ด ์‹œ์ž‘ ์‹ฌ๋ณผ๋กœ๋ถ€ํ„ฐ ๋„๋‹ฌ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธ + val reachableSymbols = findReachableSymbols(grammar.productions, grammar.startSymbol) + if (!grammar.nonTerminals.all { it in reachableSymbols }) { + return false + } + + // ์ขŒ์žฌ๊ท€๊ฐ€ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌ๋˜๋Š”์ง€ ํ™•์ธ + if (hasProblematicLeftRecursion(grammar.productions)) { + return false + } + + return true + } + + /** + * ํŒŒ์‹ฑ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasParsingConsistency(grammar: Grammar): Boolean { + // LR ํŒŒ์‹ฑ ๊ฐ€๋Šฅ์„ฑ ํ™•์ธ + return isLRParsable(grammar.productions) && + hasValidFirstFollowSets(grammar.productions, grammar.terminals, grammar.nonTerminals) && + hasNoAmbiguity(grammar.productions) + } + + /** + * ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasTerminalConsistency(grammar: Grammar): Boolean { + // ๋ชจ๋“  ํ„ฐ๋ฏธ๋„์ด ์‹ค์ œ๋กœ ์‚ฌ์šฉ๋˜๋Š”์ง€ ํ™•์ธ + val usedTerminals = grammar.productions.flatMap { it.right } + .filter { grammar.isTerminal(it) } + .toSet() + + // ์‚ฌ์šฉ๋˜์ง€ ์•Š๋Š” ํ„ฐ๋ฏธ๋„์ด ์žˆ์–ด๋„ ์ผ๊ด€์„ฑ์—๋Š” ๋ฌธ์ œ์—†์Œ (๊ฒฝ๊ณ ๋งŒ) + return true + } + + /** + * ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasNonTerminalConsistency(grammar: Grammar): Boolean { + // ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„์— ๋Œ€ํ•œ ์ƒ์‚ฐ ๊ทœ์น™์ด ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธ + val definedNonTerminals = grammar.productions.map { it.left }.toSet() + if (!grammar.nonTerminals.all { it in definedNonTerminals }) { + return false + } + + // ์šฐ๋ณ€์— ์‚ฌ์šฉ๋˜์ง€๋งŒ ์ •์˜๋˜์ง€ ์•Š์€ ๋…ผํ„ฐ๋ฏธ๋„์ด ์—†๋Š”์ง€ ํ™•์ธ + val usedNonTerminals = grammar.productions.flatMap { it.right } + .filter { grammar.isNonTerminal(it) } + .toSet() + + if (!usedNonTerminals.all { it in grammar.nonTerminals }) { + return false + } + + return true + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasProductionConsistency(grammar: Grammar): Boolean { + return hasValidProductionIDs(grammar.productions) && + hasValidProductionLengths(grammar.productions) && + hasValidASTBuilderAssignment(grammar.productions) + } + + /** + * ์‹œ์ž‘ ์‹ฌ๋ณผ์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasStartSymbolConsistency(grammar: Grammar): Boolean { + // ์‹œ์ž‘ ์‹ฌ๋ณผ์ด ์šฐ๋ณ€์— ๋‚˜ํƒ€๋‚˜์ง€ ์•Š๋Š”์ง€ ํ™•์ธ (ํ™•์žฅ ๋ฌธ๋ฒ• ์ œ์™ธ) + val startSymbolInRightSide = grammar.productions + .filter { it.id != -1 } // ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™ ์ œ์™ธ + .any { grammar.startSymbol in it.right } + + return !startSymbolInRightSide + } + + /** + * ์‹ฌ๋ณผ ์‚ฌ์šฉ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidSymbolUsage( + productions: List, + terminals: Set, + nonTerminals: Set + ): Boolean { + val allSymbols = terminals + nonTerminals + + return productions.all { production -> + production.left in nonTerminals && + production.right.all { it in allSymbols } + } + } + + /** + * ์ค‘๋ณต ์ƒ์‚ฐ ๊ทœ์น™์ด ์—†๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasNoDuplicateProductions(productions: List): Boolean { + val productionStrings = productions.map { "${it.left}->${it.right.joinToString(",")}" } + return productionStrings.size == productionStrings.toSet().size + } + + /** + * ์ƒ์‚ฐ ๊ทœ์น™ ๊ตฌ์กฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidProductionStructure(productions: List): Boolean { + return productions.all { production -> + production.id >= -1 && // -1์€ ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™์šฉ + production.right.size <= 10 && // ํ•ฉ๋ฆฌ์ ์ธ ์ตœ๋Œ€ ๊ธธ์ด + production.left != TokenType.EPSILON // ์ขŒ๋ณ€์€ ์—ก์‹ค๋ก ์ด ๋  ์ˆ˜ ์—†์Œ + } + } + + /** + * AST ๋นŒ๋”์˜ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasConsistentASTBuilders(productions: List): Boolean { + return productions.all { production -> + // ๊ฐ ์ƒ์‚ฐ ๊ทœ์น™์— ์ ์ ˆํ•œ ๋นŒ๋”๊ฐ€ ํ• ๋‹น๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ + production.astBuilder != null + } + } + + /** + * ์ˆœํ™˜ ์˜์กด์„ฑ์ด ์—†๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasNoCyclicDependencies(productions: List): Boolean { + val graph = buildDependencyGraph(productions) + return !hasCycle(graph) + } + + /** + * ์˜์กด์„ฑ ๊ตฌ์กฐ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidDependencyStructure(productions: List): Boolean { + val dependencies = productions.map { it.left to it.right.filter { sym -> sym.isNonTerminal() } } + + return dependencies.all { (left, rightNonTerminals) -> + rightNonTerminals.size <= MAX_SYMBOL_DEPENDENCIES && + rightNonTerminals.all { it != left || isValidSelfReference(left, productions) } + } + } + + /** + * ์˜์กด์„ฑ์ด ๋„๋‹ฌ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasReachableDependencies(productions: List): Boolean { + val definedSymbols = productions.map { it.left }.toSet() + val usedSymbols = productions.flatMap { it.right }.filter { it.isNonTerminal() }.toSet() + + return usedSymbols.all { it in definedSymbols } + } + + /** + * ์˜์กด์„ฑ์ด ์œ ํ•œํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasFiniteDependencies(productions: List): Boolean { + // ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„์ด ๊ฒฐ๊ตญ ํ„ฐ๋ฏธ๋„๋กœ ์œ ๋„๋  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธ + val terminals = productions.flatMap { it.right }.filter { it.isTerminal }.toSet() + val productiveSymbols = findProductiveSymbols(productions, terminals) + val nonTerminals = productions.map { it.left }.toSet() + + return nonTerminals.all { it in productiveSymbols } + } + + /** + * ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ์ขŒ์žฌ๊ท€๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasProblematicLeftRecursion(productions: List): Boolean { + // ์ง์ ‘ ์ขŒ์žฌ๊ท€ ํ™•์ธ + val directLeftRecursive = productions.filter { it.isDirectLeftRecursive() } + + // ๊ฐ„์ ‘ ์ขŒ์žฌ๊ท€ ํ™•์ธ + val graph = buildDependencyGraph(productions) + val indirectLeftRecursive = graph.keys.filter { symbol -> + hasLeftRecursivePath(symbol, symbol, graph, mutableSetOf()) + } + + // ์ขŒ์žฌ๊ท€๊ฐ€ ์žˆ์ง€๋งŒ ์ ์ ˆํžˆ ์ฒ˜๋ฆฌ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ๋ฅผ ์ฐพ์Œ + return (directLeftRecursive.isNotEmpty() || indirectLeftRecursive.isNotEmpty()) && + !hasLeftRecursionResolution(productions) + } + + /** + * LR ํŒŒ์‹ฑ ๊ฐ€๋Šฅ์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isLRParsable(productions: List): Boolean { + // ๊ธฐ๋ณธ์ ์ธ LR ์กฐ๊ฑด๋“ค ํ™•์ธ + return hasValidLRStructure(productions) && + hasNoReduceReduceConflicts(productions) && + hasResolvableShiftReduceConflicts(productions) + } + + /** + * FIRST/FOLLOW ์ง‘ํ•ฉ์˜ ์œ ํšจ์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidFirstFollowSets( + productions: List, + terminals: Set, + nonTerminals: Set + ): Boolean { + try { + calculateFirstSets(productions, terminals, nonTerminals) + calculateFollowSets(productions, terminals, nonTerminals) + return true + } catch (e: Exception) { + return false + } + } + + /** + * ๋ชจํ˜ธ์„ฑ์ด ์—†๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasNoAmbiguity(productions: List): Boolean { + // ๊ธฐ๋ณธ์ ์ธ ๋ชจํ˜ธ์„ฑ ๊ฒ€์‚ฌ + val groupedByLeft = productions.groupBy { it.left } + + return groupedByLeft.all { (_, rules) -> + if (rules.size <= 1) true + else { + val firstSymbols = rules.mapNotNull { it.right.firstOrNull() } + firstSymbols.size == firstSymbols.toSet().size + } + } + } + + /** + * Helper Methods + */ + + private fun findProductiveSymbols( + productions: List, + terminals: Set + ): Set { + val productive = terminals.toMutableSet() + var changed = true + + while (changed) { + changed = false + for (production in productions) { + if (production.left !in productive && + production.right.all { it in productive }) { + productive.add(production.left) + changed = true + } + } + } + + return productive + } + + private fun findReachableSymbols( + productions: List, + startSymbol: TokenType + ): Set { + val reachable = mutableSetOf() + val queue = mutableListOf(startSymbol) + + while (queue.isNotEmpty()) { + val current = queue.removeAt(0) + if (current in reachable) continue + + reachable.add(current) + + productions.filter { it.left == current }.forEach { production -> + production.right.forEach { symbol -> + if (symbol !in reachable) { + queue.add(symbol) + } + } + } + } + + return reachable + } + + private fun buildDependencyGraph(productions: List): Map> { + val graph = mutableMapOf>() + + productions.forEach { production -> + val dependencies = graph.getOrPut(production.left) { mutableSetOf() } + production.right.filter { it.isNonTerminal() }.forEach { symbol -> + dependencies.add(symbol) + } + } + + return graph + } + + private fun hasCycle(graph: Map>): Boolean { + val visited = mutableSetOf() + val recursionStack = mutableSetOf() + + for (node in graph.keys) { + if (hasCycleDFS(node, graph, visited, recursionStack)) { + return true + } + } + + return false + } + + private fun hasCycleDFS( + node: TokenType, + graph: Map>, + visited: MutableSet, + recursionStack: MutableSet + ): Boolean { + visited.add(node) + recursionStack.add(node) + + val neighbors = graph[node] ?: emptySet() + for (neighbor in neighbors) { + if (neighbor !in visited) { + if (hasCycleDFS(neighbor, graph, visited, recursionStack)) { + return true + } + } else if (neighbor in recursionStack) { + return true + } + } + + recursionStack.remove(node) + return false + } + + private fun isValidSelfReference(symbol: TokenType, productions: List): Boolean { + // ์ž๊ธฐ ์ฐธ์กฐ๊ฐ€ ์šฐ์žฌ๊ท€ ํ˜•ํƒœ์ธ์ง€ ํ™•์ธ + val selfReferencingProductions = productions.filter { + it.left == symbol && symbol in it.right + } + + return selfReferencingProductions.all { production -> + val symbolIndex = production.right.indexOf(symbol) + symbolIndex > 0 // ์ขŒ์žฌ๊ท€๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ๋งŒ ํ—ˆ์šฉ + } + } + + private fun hasLeftRecursivePath( + start: TokenType, + current: TokenType, + graph: Map>, + visited: MutableSet + ): Boolean { + if (current in visited) return current == start + + visited.add(current) + val neighbors = graph[current] ?: emptySet() + + for (neighbor in neighbors) { + if (hasLeftRecursivePath(start, neighbor, graph, visited)) { + return true + } + } + + visited.remove(current) + return false + } + + private fun hasLeftRecursionResolution(productions: List): Boolean { + // ์ขŒ์žฌ๊ท€๊ฐ€ ์ ์ ˆํ•œ ์šฐ์žฌ๊ท€๋กœ ๋ณ€ํ™˜๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + // ์ด๋Š” ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ๋” ๋ณต์žกํ•œ ๋ถ„์„์ด ํ•„์š” + return true // ๊ฐ„๋‹จํ•œ ๊ตฌํ˜„ + } + + private fun hasValidLRStructure(productions: List): Boolean { + // LR ๋ฌธ๋ฒ•์˜ ๊ธฐ๋ณธ ์กฐ๊ฑด๋“ค ํ™•์ธ + return productions.all { it.right.size <= 10 } // ์ ์ ˆํ•œ ์ƒ์‚ฐ ๊ทœ์น™ ๊ธธ์ด + } + + private fun hasNoReduceReduceConflicts(productions: List): Boolean { + // Reduce-Reduce ์ถฉ๋Œ ๊ฒ€์‚ฌ (๊ฐ„๋‹จํ•œ ๋ฒ„์ „) + return true // ์‹ค์ œ๋กœ๋Š” ๋” ๋ณต์žกํ•œ ๋ถ„์„ ํ•„์š” + } + + private fun hasResolvableShiftReduceConflicts(productions: List): Boolean { + // Shift-Reduce ์ถฉ๋Œ์ด ํ•ด๊ฒฐ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธ (๊ฐ„๋‹จํ•œ ๋ฒ„์ „) + return true // ์‹ค์ œ๋กœ๋Š” ์šฐ์„ ์ˆœ์œ„์™€ ๊ฒฐํ•ฉ์„ฑ ๋ถ„์„ ํ•„์š” + } + + private fun hasValidProductionIDs(productions: List): Boolean { + val ids = productions.map { it.id } + return ids.all { it >= -1 } && ids.toSet().size == ids.size + } + + private fun hasValidProductionLengths(productions: List): Boolean { + return productions.all { it.right.size <= 20 } // ํ•ฉ๋ฆฌ์ ์ธ ์ตœ๋Œ€ ๊ธธ์ด + } + + private fun hasValidASTBuilderAssignment(productions: List): Boolean { + return productions.all { it.astBuilder != null } + } + + private fun calculateFirstSets( + productions: List, + terminals: Set, + nonTerminals: Set + ): Map> { + // FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + val firstSets = mutableMapOf>() + + // ํ„ฐ๋ฏธ๋„์˜ FIRST ์ง‘ํ•ฉ ์ดˆ๊ธฐํ™” + terminals.forEach { firstSets[it] = mutableSetOf(it) } + nonTerminals.forEach { firstSets[it] = mutableSetOf() } + + var changed = true + while (changed) { + changed = false + for (production in productions) { + val oldSize = firstSets[production.left]?.size ?: 0 + // FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ ๋กœ์ง... + val newSize = firstSets[production.left]?.size ?: 0 + if (newSize > oldSize) changed = true + } + } + + return firstSets + } + + private fun calculateFollowSets( + productions: List, + terminals: Set, + nonTerminals: Set + ): Map> { + // FOLLOW ์ง‘ํ•ฉ ๊ณ„์‚ฐ (๊ฐ„๋‹จํ•œ ๊ตฌํ˜„) + return emptyMap() // ์‹ค์ œ ๊ตฌํ˜„ ํ•„์š” + } + + /** + * ๋ช…์„ธ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getSpecificationInfo(): Map = mapOf( + "name" to GrammarConsistencySpecConstants.NAME, + "maxRecursionDepth" to GrammarConsistencySpecConstants.MAX_RECURSION_DEPTH, + "maxDerivationSteps" to GrammarConsistencySpecConstants.MAX_DERIVATION_STEPS, + "maxSymbolDependencies" to GrammarConsistencySpecConstants.MAX_SYMBOL_DEPENDENCIES, + "supportedValidations" to GrammarConsistencySpecConstants.SUPPORTED_VALIDATIONS + ) + + object GrammarConsistencySpecConstants { + const val NAME = "GrammarConsistencySpec" + const val MAX_RECURSION_DEPTH = 50 // ๊ธฐ์กด ๊ฐ’ ์œ ์ง€ + const val MAX_DERIVATION_STEPS = 1000 + const val MAX_SYMBOL_DEPENDENCIES = 200 + + val SUPPORTED_VALIDATIONS = listOf( + "structuralConsistency", + "semanticConsistency", + "parsingConsistency", + "terminalConsistency", + "nonTerminalConsistency", + "productionConsistency", + "startSymbolConsistency", + "dependencyConsistency" + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/LRParsingValiditySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/LRParsingValiditySpec.kt new file mode 100644 index 00000000..ebdc81ef --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/LRParsingValiditySpec.kt @@ -0,0 +1,515 @@ +package hs.kr.entrydsm.domain.parser.specifications + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.LRItem +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.values.Grammar +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority +import hs.kr.entrydsm.global.constants.ErrorCodes + +/** + * POC ์ฝ”๋“œ์˜ ์™„์ „ํ•œ LR(1) ํŒŒ์„œ ๊ฒ€์ฆ ๊ธฐ๋Šฅ์„ DDD Specification ํŒจํ„ด์œผ๋กœ ๊ตฌํ˜„ํ•œ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * POC ์ฝ”๋“œ์˜ Grammar, LRParserTable, RealLRParser์˜ ํ•ต์‹ฌ ๊ฒ€์ฆ ๋กœ์ง์„ + * ์ฒด๊ณ„์ ์ด๊ณ  ํ™•์žฅ ๊ฐ€๋Šฅํ•œ ๋ช…์„ธ ํŒจํ„ด์œผ๋กœ ์žฌ๊ตฌ์„ฑํ–ˆ์Šต๋‹ˆ๋‹ค. LR(1) ์•„์ดํ…œ ์ง‘ํ•ฉ, + * FIRST/FOLLOW ๊ณ„์‚ฐ, DFA ์ƒํƒœ ๊ตฌ์ถ•, ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ฒ€์ฆ ๋“ฑ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.28 + */ +@Specification( + name = "LRParsingValidity", + description = "POC ์ฝ”๋“œ ๊ธฐ๋ฐ˜์˜ ์™„์ „ํ•œ LR(1) ํŒŒ์„œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ๋ช…์„ธ", + domain = "parser", + priority = Priority.CRITICAL +) +class LRParsingValiditySpec { + + companion object { + // POC ์ฝ”๋“œ์˜ Grammar์—์„œ ์ •์˜๋œ ํ† ํฐ๋“ค + private val TERMINALS = setOf( + TokenType.NUMBER, TokenType.BOOLEAN, TokenType.VARIABLE, + TokenType.PLUS, TokenType.MINUS, TokenType.MULTIPLY, TokenType.DIVIDE, + TokenType.MODULO, TokenType.POWER, TokenType.EQUAL, TokenType.NOT_EQUAL, + TokenType.LESS_THAN, TokenType.LESS_EQUAL, TokenType.GREATER_THAN, + TokenType.GREATER_EQUAL, TokenType.AND, TokenType.OR, TokenType.NOT, + TokenType.LEFT_PAREN, TokenType.RIGHT_PAREN, TokenType.COMMA, + TokenType.FUNCTION, TokenType.IF, TokenType.QUESTION, TokenType.COLON, + TokenType.WHITESPACE, TokenType.DOLLAR + ) + + private val NON_TERMINALS = setOf( + TokenType.EXPR, TokenType.AND_EXPR, TokenType.EQUALITY_EXPR, + TokenType.RELATIONAL_EXPR, TokenType.ADDITIVE_EXPR, + TokenType.MULTIPLICATIVE_EXPR, TokenType.UNARY_EXPR, + TokenType.POWER_EXPR, TokenType.PRIMARY_EXPR, TokenType.ATOM, + TokenType.FUNCTION_CALL, TokenType.ARGUMENTS, TokenType.ARGUMENT_LIST, + TokenType.CONDITIONAL_EXPR, TokenType.START + ) + + // POC ์ฝ”๋“œ์˜ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ (๋‚ฎ์€ ์ˆซ์ž๊ฐ€ ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + private val OPERATOR_PRECEDENCE = mapOf( + TokenType.OR to 1, + TokenType.AND to 2, + TokenType.EQUAL to 3, TokenType.NOT_EQUAL to 3, + TokenType.LESS_THAN to 4, TokenType.LESS_EQUAL to 4, + TokenType.GREATER_THAN to 4, TokenType.GREATER_EQUAL to 4, + TokenType.PLUS to 5, TokenType.MINUS to 5, + TokenType.MULTIPLY to 6, TokenType.DIVIDE to 6, TokenType.MODULO to 6, + TokenType.POWER to 7, + TokenType.NOT to 8 + ) + } + + /** + * POC ์ฝ”๋“œ์˜ Grammar ์œ ํšจ์„ฑ ๊ฒ€์ฆ + */ + fun isSatisfiedBy(grammar: Grammar): Boolean { + return try { + validateProductions(grammar.productions) && + validateStartSymbol(grammar.startSymbol) && + validateTerminals(grammar.terminals) && + validateNonTerminals(grammar.nonTerminals) && + validateGrammarConsistency(grammar) && + validateOperatorPrecedence(grammar) + } catch (e: Exception) { + false + } + } + + /** + * POC ์ฝ”๋“œ์˜ LR(1) ์•„์ดํ…œ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + */ + fun isSatisfiedBy(item: LRItem): Boolean { + return try { + validateItemProduction(item.production) && + validateItemPosition(item.dotPos, item.production) && + validateLookahead(item.lookahead) && + validateItemConsistency(item) + } catch (e: Exception) { + false + } + } + + /** + * POC ์ฝ”๋“œ์˜ ํ† ํฐ ์‹œํ€€์Šค ํŒŒ์‹ฑ ๊ฐ€๋Šฅ์„ฑ ๊ฒ€์ฆ + */ + fun isSatisfiedBy(tokens: List): Boolean { + return try { + validateTokenSequence(tokens) && + validateTokenTypes(tokens) && + validateTokenPositions(tokens) && + validateParenthesesBalance(tokens) && + validateOperatorSequence(tokens) + } catch (e: Exception) { + false + } + } + + /** + * POC ์ฝ”๋“œ์˜ LR Action ์œ ํšจ์„ฑ ๊ฒ€์ฆ + */ + fun isSatisfiedBy(action: LRAction, state: Int, token: TokenType): Boolean { + return try { + when (action) { + is LRAction.Shift -> validateShiftAction(action, state, token) + is LRAction.Reduce -> validateReduceAction(action, state, token) + is LRAction.Accept -> validateAcceptAction(action, state, token) + is LRAction.Error -> validateErrorAction(action, state, token) + } + } catch (e: Exception) { + false + } + } + + // Grammar validation methods + + private fun validateProductions(productions: List): Boolean { + if (productions.isEmpty()) return false + + return productions.all { production -> + validateProductionStructure(production) && + validateProductionSymbols(production) + } + } + + private fun validateProductionStructure(production: Production): Boolean { + return production.left in NON_TERMINALS && + production.right.isNotEmpty() && + production.id >= 0 + } + + private fun validateProductionSymbols(production: Production): Boolean { + return production.right.all { symbol -> + symbol in TERMINALS || symbol in NON_TERMINALS + } + } + + private fun validateStartSymbol(startSymbol: TokenType): Boolean { + return startSymbol in NON_TERMINALS + } + + private fun validateTerminals(terminals: Set): Boolean { + return terminals.isNotEmpty() && + terminals.all { it in TERMINALS } && + TokenType.DOLLAR in terminals + } + + private fun validateNonTerminals(nonTerminals: Set): Boolean { + return nonTerminals.isNotEmpty() && + nonTerminals.all { it in NON_TERMINALS } + } + + private fun validateGrammarConsistency(grammar: Grammar): Boolean { + // ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™์˜ ์™ผ์ชฝ ์‹ฌ๋ณผ์ด non-terminal์ธ์ง€ ํ™•์ธ + val leftSymbols = grammar.productions.map { it.left }.toSet() + val rightSymbols = grammar.productions.flatMap { it.right }.toSet() + + // ์‹œ์ž‘ ์‹ฌ๋ณผ์—์„œ ๋„๋‹ฌ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์‹ฌ๋ณผ ํ™•์ธ + val reachableSymbols = calculateReachableSymbols(grammar) + + return leftSymbols.all { it in grammar.nonTerminals } && + (leftSymbols intersect grammar.terminals).isEmpty() && + reachableSymbols.containsAll(grammar.terminals) && + reachableSymbols.containsAll(grammar.nonTerminals) + } + + private fun validateOperatorPrecedence(grammar: Grammar): Boolean { + // POC ์ฝ”๋“œ์˜ ๋ฌธ๋ฒ•์ด ์˜ฌ๋ฐ”๋ฅธ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ + val operatorProductions = grammar.productions.filter { production -> + production.right.any { symbol -> symbol in OPERATOR_PRECEDENCE.keys } + } + + return operatorProductions.all { production -> + validateProductionPrecedence(production) + } + } + + // LR Item validation methods + + private fun validateItemProduction(production: Production): Boolean { + return validateProductionStructure(production) && + validateProductionSymbols(production) + } + + private fun validateItemPosition(position: Int, production: Production): Boolean { + return position >= 0 && position <= production.right.size + } + + private fun validateLookahead(lookahead: TokenType): Boolean { + return lookahead in TERMINALS + } + + private fun validateItemConsistency(item: LRItem): Boolean { + // ์•„์ดํ…œ์˜ ์œ„์น˜๊ฐ€ ์ƒ์‚ฐ ๊ทœ์น™์˜ ๋์ด๋ฉด Reduce ๊ฐ€๋Šฅ + // ์•„์ดํ…œ์˜ ์œ„์น˜๊ฐ€ ์ƒ์‚ฐ ๊ทœ์น™ ์ค‘๊ฐ„์ด๋ฉด Shift ๊ฐ€๋Šฅ + val isAtEnd = item.dotPos >= item.production.right.size + val nextSymbol = if (isAtEnd) null else item.production.right[item.dotPos] + + return when { + isAtEnd -> item.lookahead in TERMINALS + nextSymbol != null -> nextSymbol in TERMINALS || nextSymbol in NON_TERMINALS + else -> false + } + } + + // Token sequence validation methods + + private fun validateTokenSequence(tokens: List): Boolean { + if (tokens.isEmpty()) return false + + // POC ์ฝ”๋“œ์ฒ˜๋Ÿผ ๋งˆ์ง€๋ง‰ ํ† ํฐ์ด DOLLAR์ธ์ง€ ํ™•์ธ + val lastToken = tokens.lastOrNull() + return lastToken?.type == TokenType.DOLLAR + } + + private fun validateTokenTypes(tokens: List): Boolean { + return tokens.all { token -> + token.type in TERMINALS || token.type in NON_TERMINALS + } + } + + private fun validateTokenPositions(tokens: List): Boolean { + // ํ† ํฐ๋“ค์˜ ์œ„์น˜๊ฐ€ ์ˆœ์ฐจ์ ์œผ๋กœ ์ฆ๊ฐ€ํ•˜๋Š”์ง€ ํ™•์ธ (POC ์ฝ”๋“œ ๊ธฐ๋ฐ˜) + return tokens.zipWithNext().all { (current, next) -> + current.position.index <= next.position.index + } + } + + private fun validateParenthesesBalance(tokens: List): Boolean { + var balance = 0 + for (token in tokens) { + when (token.type) { + TokenType.LEFT_PAREN -> balance++ + TokenType.RIGHT_PAREN -> balance-- + else -> { /* ignore other tokens */ } + } + if (balance < 0) return false + } + return balance == 0 + } + + private fun validateOperatorSequence(tokens: List): Boolean { + // ์—ฐ์‚ฐ์ž๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ์ˆœ์„œ๋กœ ๋‚˜ํƒ€๋‚˜๋Š”์ง€ ํ™•์ธ + val operatorTypes = OPERATOR_PRECEDENCE.keys + var lastWasOperator = false + + for (token in tokens) { + val isOperator = token.type in operatorTypes + + if (isOperator && lastWasOperator) { + // ์—ฐ์†๋œ ์ดํ•ญ ์—ฐ์‚ฐ์ž๋Š” ํ—ˆ์šฉ๋˜์ง€ ์•Š์Œ (๋‹จํ•ญ ์—ฐ์‚ฐ์ž ์ œ์™ธ) + if (token.type != TokenType.NOT && token.type != TokenType.MINUS) { + return false + } + } + + lastWasOperator = isOperator && token.type != TokenType.NOT + } + + return true + } + + // LR Action validation methods + + private fun validateShiftAction(action: LRAction.Shift, state: Int, token: TokenType): Boolean { + return action.state >= 0 && + token in TERMINALS && + state >= 0 + } + + private fun validateReduceAction(action: LRAction.Reduce, state: Int, token: TokenType): Boolean { + return action.production.id >= 0 && + action.production.left in NON_TERMINALS && + token in TERMINALS + } + + private fun validateAcceptAction(action: LRAction.Accept, state: Int, token: TokenType): Boolean { + return token == TokenType.DOLLAR && + state >= 0 + } + + private fun validateErrorAction(action: LRAction.Error, state: Int, token: TokenType): Boolean { + return action.errorMessage?.isNotBlank() ?: true + } + + // Helper methods + + private fun calculateReachableSymbols(grammar: Grammar): Set { + val reachable = mutableSetOf() + val worklist = mutableSetOf(grammar.startSymbol) + + while (worklist.isNotEmpty()) { + val symbol = worklist.first() + worklist.remove(symbol) + if (symbol in reachable) continue + + reachable.add(symbol) + + // ์ด ์‹ฌ๋ณผ์„ ์™ผ์ชฝ์— ๊ฐ€์ง„ ๋ชจ๋“  ์ƒ์‚ฐ ๊ทœ์น™ ์ฐพ๊ธฐ + val productions = grammar.productions.filter { it.left == symbol } + for (production in productions) { + worklist.addAll(production.right - reachable) + } + } + + return reachable + } + + private fun validateProductionPrecedence(production: Production): Boolean { + val operators = production.right.filter { it in OPERATOR_PRECEDENCE.keys } + if (operators.size <= 1) return true + + // ์—ฐ์‚ฐ์ž๋“ค์ด ์˜ฌ๋ฐ”๋ฅธ ์šฐ์„ ์ˆœ์œ„ ์ˆœ์„œ๋กœ ๋ฐฐ์น˜๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธ + return operators.zipWithNext().all { (left, right) -> + val leftPrec = OPERATOR_PRECEDENCE[left] ?: Int.MAX_VALUE + val rightPrec = OPERATOR_PRECEDENCE[right] ?: Int.MAX_VALUE + leftPrec <= rightPrec + } + } + + /** + * ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ธํžˆ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getValidationErrors(grammar: Grammar): List { + val errors = mutableListOf() + + try { + if (!validateProductions(grammar.productions)) { + errors.add(ValidationError( + ErrorCodes.Parser.INVALID_PRODUCTION.code, + LRParsingValiditySpecConstants.MSG_INVALID_PRODUCTION, + ValidationError.Severity.ERROR + )) + } + + if (!validateStartSymbol(grammar.startSymbol)) { + errors.add(ValidationError( + ErrorCodes.Parser.GRAMMAR_VIOLATION.code, + LRParsingValiditySpecConstants.MSG_INVALID_START_SYMBOL.format(grammar.startSymbol), + ValidationError.Severity.ERROR + )) + } + + if (!validateGrammarConsistency(grammar)) { + errors.add(ValidationError( + ErrorCodes.Parser.GRAMMAR_VIOLATION.code, + LRParsingValiditySpecConstants.MSG_GRAMMAR_CONSISTENCY_FAIL, + ValidationError.Severity.CRITICAL + )) + } + + if (!validateOperatorPrecedence(grammar)) { + errors.add(ValidationError( + ErrorCodes.Parser.GRAMMAR_VIOLATION.code, + LRParsingValiditySpecConstants.MSG_OPERATOR_PRECEDENCE_FAIL, + ValidationError.Severity.WARNING + )) + } + + } catch (e: Exception) { + errors.add(ValidationError( + ErrorCodes.Common.UNKNOWN_ERROR.code, + LRParsingValiditySpecConstants.MSG_UNKNOWN_GRAMMAR_ERROR.format(e.message), + ValidationError.Severity.CRITICAL + )) + } + + return errors + } + + /** + * ํ† ํฐ ์‹œํ€€์Šค ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ์ƒ์„ธํžˆ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getTokenValidationErrors(tokens: List): List { + val errors = mutableListOf() + + try { + if (!validateTokenSequence(tokens)) { + errors.add(ValidationError( + ErrorCodes.Lexer.UNEXPECTED_TOKEN.code, + LRParsingValiditySpecConstants.MSG_INVALID_TOKEN_SEQUENCE, + ValidationError.Severity.ERROR + )) + } + + if (!validateParenthesesBalance(tokens)) { + errors.add(ValidationError( + ErrorCodes.Parser.SYNTAX_ERROR.code, + LRParsingValiditySpecConstants.MSG_PARENTHESIS_UNBALANCED, + ValidationError.Severity.ERROR + )) + } + + if (!validateOperatorSequence(tokens)) { + errors.add(ValidationError( + ErrorCodes.Parser.SYNTAX_ERROR.code, + LRParsingValiditySpecConstants.MSG_OPERATOR_SEQUENCE_INVALID, + ValidationError.Severity.ERROR + )) + } + + } catch (e: Exception) { + errors.add(ValidationError( + ErrorCodes.Common.UNKNOWN_ERROR.code, + LRParsingValiditySpecConstants.MSG_UNKNOWN_TOKEN_ERROR.format(e.message), + ValidationError.Severity.CRITICAL + )) + } + + return errors + } + + /** + * ๊ฒ€์ฆ ์˜ค๋ฅ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ + data class ValidationError( + val code: String, + val message: String, + val severity: Severity = Severity.ERROR + ) { + enum class Severity { + INFO, WARNING, ERROR, CRITICAL + } + } + + /** + * ๋ช…์„ธ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getConfiguration(): Map = mapOf( + LRParsingValiditySpecConstants.CFG_NAME to LRParsingValiditySpecConstants.SPEC_NAME, + LRParsingValiditySpecConstants.CFG_BASED_ON to LRParsingValiditySpecConstants.BASED_ON, + LRParsingValiditySpecConstants.CFG_TERMINALS to TERMINALS.size, + LRParsingValiditySpecConstants.CFG_NON_TERMINALS to NON_TERMINALS.size, + LRParsingValiditySpecConstants.CFG_OPERATOR_PRECEDENCE_LEVELS to OPERATOR_PRECEDENCE.values.toSet().size, + LRParsingValiditySpecConstants.CFG_GRAMMAR_VALIDATION to true, + LRParsingValiditySpecConstants.CFG_ITEM_VALIDATION to true, + LRParsingValiditySpecConstants.CFG_TOKEN_VALIDATION to true, + LRParsingValiditySpecConstants.CFG_ACTION_VALIDATION to true, + LRParsingValiditySpecConstants.CFG_PRECEDENCE_VALIDATION to true + ) + + /** + * ๋ช…์„ธ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getStatistics(): Map = mapOf( + LRParsingValiditySpecConstants.STAT_SPECIFICATION_NAME to LRParsingValiditySpecConstants.SPEC_NAME, + LRParsingValiditySpecConstants.STAT_IMPLEMENTED_FEATURES to LRParsingValiditySpecConstants.IMPLEMENTED_FEATURES_LIST, + LRParsingValiditySpecConstants.STAT_POC_COMPATIBILITY to true, + LRParsingValiditySpecConstants.STAT_PARSER_TYPE to "LR(1)", + LRParsingValiditySpecConstants.STAT_VALIDATION_LAYERS to 5, + LRParsingValiditySpecConstants.STAT_PRIORITY to Priority.CRITICAL.name + ) + + object LRParsingValiditySpecConstants { + // Specification Info + const val SPEC_NAME = "LRParsingValiditySpec" + const val BASED_ON = "POC_LR1_Parser" + + // Configuration Keys + const val CFG_NAME = "name" + const val CFG_BASED_ON = "based_on" + const val CFG_TERMINALS = "terminals" + const val CFG_NON_TERMINALS = "nonTerminals" + const val CFG_OPERATOR_PRECEDENCE_LEVELS = "operatorPrecedenceLevels" + const val CFG_GRAMMAR_VALIDATION = "grammarValidation" + const val CFG_ITEM_VALIDATION = "itemValidation" + const val CFG_TOKEN_VALIDATION = "tokenValidation" + const val CFG_ACTION_VALIDATION = "actionValidation" + const val CFG_PRECEDENCE_VALIDATION = "precedenceValidation" + + // Statistics Keys + const val STAT_SPECIFICATION_NAME = "specificationName" + const val STAT_IMPLEMENTED_FEATURES = "implementedFeatures" + const val STAT_POC_COMPATIBILITY = "pocCompatibility" + const val STAT_PARSER_TYPE = "parserType" + const val STAT_VALIDATION_LAYERS = "validationLayers" + const val STAT_PRIORITY = "priority" + + // Messages + const val MSG_INVALID_PRODUCTION = "์ƒ์‚ฐ ๊ทœ์น™์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val MSG_INVALID_START_SYMBOL = "์‹œ์ž‘ ์‹ฌ๋ณผ์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค: %s" + const val MSG_GRAMMAR_CONSISTENCY_FAIL = "๋ฌธ๋ฒ• ์ผ๊ด€์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ" + const val MSG_OPERATOR_PRECEDENCE_FAIL = "์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๊ฒ€์ฆ ์‹คํŒจ" + const val MSG_UNKNOWN_GRAMMAR_ERROR = "๋ฌธ๋ฒ• ๊ฒ€์ฆ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: %s" + const val MSG_INVALID_TOKEN_SEQUENCE = "ํ† ํฐ ์‹œํ€€์Šค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค (DOLLAR ํ† ํฐ ๋ˆ„๋ฝ)" + const val MSG_PARENTHESIS_UNBALANCED = "๊ด„ํ˜ธ๊ฐ€ ๊ท ํ˜•์„ ์ด๋ฃจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val MSG_OPERATOR_SEQUENCE_INVALID = "์—ฐ์‚ฐ์ž ์‹œํ€€์Šค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val MSG_UNKNOWN_TOKEN_ERROR = "ํ† ํฐ ๊ฒ€์ฆ ์ค‘ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: %s" + + // Implemented Features List + val IMPLEMENTED_FEATURES_LIST = listOf( + "grammar_validation", + "lr_item_validation", + "token_sequence_validation", + "lr_action_validation", + "precedence_validation", + "consistency_validation" + ) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/ParsingTableValiditySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/ParsingTableValiditySpec.kt new file mode 100644 index 00000000..b91c52cf --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/ParsingTableValiditySpec.kt @@ -0,0 +1,151 @@ +package hs.kr.entrydsm.domain.parser.specifications + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.values.LRAction +import hs.kr.entrydsm.domain.parser.values.ParsingTable +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.SpecificationContract +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * ParsingTable์˜ ๊ตฌ์กฐ์  ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ์ž…๋‹ˆ๋‹ค. + * + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์ด LR ํŒŒ์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์—์„œ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•  ์ˆ˜ ์žˆ๋„๋ก + * ํ•„์ˆ˜ ๊ตฌ์กฐ์  ์š”๊ตฌ์‚ฌํ•ญ๋“ค์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.13 + */ +@Specification( + name = "ParsingTableValiditySpec", + description = "ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ๊ตฌ์กฐ์  ๋ฌด๊ฒฐ์„ฑ ๋ฐ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ", + domain = "Parser", + priority = Priority.HIGH +) +class ParsingTableValiditySpec : SpecificationContract { + + object ParsingTableValiditySpecConstants { + // Spec meta + const val NAME = "ParsingTableValiditySpec" + const val DESCRIPTION = "ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ๊ตฌ์กฐ์  ๋ฌด๊ฒฐ์„ฑ ๋ฐ ์ผ๊ด€์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ" + const val DOMAIN = "Parser" + val DEFAULT_PRIORITY = Priority.HIGH + + // Messages + const val MSG_PREFIX_FAIL = "ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” ๊ฒ€์ฆ ์‹คํŒจ: " + const val MSG_BASIC_FAIL = "๊ธฐ๋ณธ ๊ตฌ์กฐ ๊ฒ€์ฆ ์‹คํŒจ" + const val MSG_BASIC_ERR = "๊ธฐ๋ณธ ๊ตฌ์กฐ ๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜: %s" + const val MSG_ACTION_FAIL = "Action ํ…Œ์ด๋ธ” ๊ฒ€์ฆ ์‹คํŒจ" + const val MSG_ACTION_ERR = "Action ํ…Œ์ด๋ธ” ๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜: %s" + const val MSG_GOTO_FAIL = "Goto ํ…Œ์ด๋ธ” ๊ฒ€์ฆ ์‹คํŒจ" + const val MSG_GOTO_ERR = "Goto ํ…Œ์ด๋ธ” ๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜: %s" + } + + override fun isSatisfiedBy(candidate: ParsingTable): Boolean { + return try { + validateBasicStructure(candidate) && + validateActionTable(candidate) && + validateGotoTable(candidate) + } catch (e: Exception) { + false + } + } + + override fun getName(): String = ParsingTableValiditySpecConstants.NAME + + override fun getDescription(): String = ParsingTableValiditySpecConstants.DESCRIPTION + + override fun getDomain(): String = ParsingTableValiditySpecConstants.DOMAIN + + override fun getPriority(): Priority = ParsingTableValiditySpecConstants.DEFAULT_PRIORITY + + override fun getErrorMessage(candidate: ParsingTable): String { + val errors = mutableListOf() + + runCatching { + if (!validateBasicStructure(candidate)) { + errors.add(ParsingTableValiditySpecConstants.MSG_BASIC_FAIL) + } + }.onFailure { + errors.add(ParsingTableValiditySpecConstants.MSG_BASIC_ERR.format(it.message)) + } + + runCatching { + if (!validateActionTable(candidate)) { + errors.add(ParsingTableValiditySpecConstants.MSG_ACTION_FAIL) + } + }.onFailure { + errors.add(ParsingTableValiditySpecConstants.MSG_ACTION_ERR.format(it.message)) + } + + runCatching { + if (!validateGotoTable(candidate)) { + errors.add(ParsingTableValiditySpecConstants.MSG_GOTO_FAIL) + } + }.onFailure { + errors.add(ParsingTableValiditySpecConstants.MSG_GOTO_ERR.format(it.message)) + } + + return ParsingTableValiditySpecConstants.MSG_PREFIX_FAIL + errors.joinToString(", ") + } + + /** + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ๊ธฐ๋ณธ ๊ตฌ์กฐ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateBasicStructure(table: ParsingTable): Boolean { + // ์ตœ์†Œ ํ•˜๋‚˜์˜ ์ƒํƒœ ํ•„์š” + if (table.states.isEmpty()) return false + + // ์‹œ์ž‘ ์ƒํƒœ๊ฐ€ ์ƒํƒœ ๋ชฉ๋ก์— ํฌํ•จ๋˜์–ด์•ผ ํ•จ + if (table.startState !in table.states) return false + + // ๋ชจ๋“  ์ˆ˜๋ฝ ์ƒํƒœ๊ฐ€ ์ƒํƒœ ๋ชฉ๋ก์— ํฌํ•จ๋˜์–ด์•ผ ํ•จ + if (!table.acceptStates.all { it in table.states }) return false + + return true + } + + /** + * Action ํ…Œ์ด๋ธ”์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateActionTable(table: ParsingTable): Boolean { + return table.actionTable.all { (key, action) -> + val (stateId, terminal) = key + + // ์ƒํƒœ ID๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธ + stateId in table.states && + // ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ธ์ง€ ํ™•์ธ + terminal.isTerminal && + // ์•ก์…˜์ด ์œ ํšจํ•œ์ง€ ํ™•์ธ + isValidAction(action, table.states.keys) + } + } + + /** + * Goto ํ…Œ์ด๋ธ”์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun validateGotoTable(table: ParsingTable): Boolean { + return table.gotoTable.all { (key, targetState) -> + val (stateId, nonTerminal) = key + + // ์ƒํƒœ ID๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธ + stateId in table.states && + // ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ธ์ง€ ํ™•์ธ + nonTerminal.isNonTerminal() && + // ๋ชฉํ‘œ ์ƒํƒœ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธ + targetState in table.states + } + } + + /** + * LR ์•ก์…˜์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isValidAction(action: LRAction, validStates: Set): Boolean { + return when (action) { + is LRAction.Shift -> action.state in validStates + is LRAction.Reduce -> action.production.left != null && action.production.right.isNotEmpty() + is LRAction.Accept -> true + is LRAction.Error -> true + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/ParsingValiditySpec.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/ParsingValiditySpec.kt new file mode 100644 index 00000000..0714434c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/specifications/ParsingValiditySpec.kt @@ -0,0 +1,586 @@ +package hs.kr.entrydsm.domain.parser.specifications + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * ๊ตฌ๋ฌธ ๋ถ„์„์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” Specification ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Specification ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ํ† ํฐ ์‹œํ€€์Šค์˜ ๊ตฌ๋ฌธ์  ์œ ํšจ์„ฑ์„ + * ๊ฒ€์ฆํ•˜๋Š” ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ  ์กฐํ•ฉ ๊ฐ€๋Šฅํ•œ ํ˜•ํƒœ๋กœ + * ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. ๋‹ค์–‘ํ•œ ํŒŒ์‹ฑ ๊ฒ€์ฆ ๊ทœ์น™์„ ๋…๋ฆฝ์ ์œผ๋กœ ์ •์˜ํ•˜๊ณ  ์กฐํ•ฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +@Specification( + name = "ParsingValidity", + description = "ํ† ํฐ ์‹œํ€€์Šค์˜ ๊ตฌ๋ฌธ์  ์œ ํšจ์„ฑ๊ณผ ํŒŒ์‹ฑ ๊ฐ€๋Šฅ์„ฑ์„ ๊ฒ€์ฆํ•˜๋Š” ๋ช…์„ธ", + domain = "parser", + priority = Priority.HIGH +) +class ParsingValiditySpec { + + companion object { + private const val MAX_TOKEN_SEQUENCE_LENGTH = 10000 + private const val MAX_NESTING_DEPTH = 100 + private const val MAX_EXPRESSION_COMPLEXITY = 500 + } + + /** + * ํ† ํฐ ์‹œํ€€์Šค๊ฐ€ ๊ตฌ๋ฌธ์ ์œผ๋กœ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ์‹œํ€€์Šค + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isSatisfiedBy(tokens: List): Boolean { + return hasValidLength(tokens) && + hasValidStructure(tokens) && + hasBalancedDelimiters(tokens) && + hasValidTokenOrder(tokens) && + hasValidNestingDepth(tokens) && + hasValidExpressionComplexity(tokens) + } + + /** + * ํ† ํฐ ์‹œํ€€์Šค๊ฐ€ ์™„์ „ํ•œ ํ‘œํ˜„์‹์ธ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ์‹œํ€€์Šค + * @return ์™„์ „ํ•œ ํ‘œํ˜„์‹์ด๋ฉด true + */ + fun isCompleteExpression(tokens: List): Boolean { + if (!isSatisfiedBy(tokens)) return false + + return hasValidStartAndEnd(tokens) && + hasNoIncompleteOperations(tokens) && + hasValidFunctionCalls(tokens) + } + + /** + * ํ† ํฐ ์‹œํ€€์Šค๊ฐ€ ๋ถ€๋ถ„ ํ‘œํ˜„์‹์œผ๋กœ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ์‹œํ€€์Šค + * @return ๋ถ€๋ถ„ ํ‘œํ˜„์‹์œผ๋กœ ์œ ํšจํ•˜๋ฉด true + */ + fun isValidPartialExpression(tokens: List): Boolean { + return hasValidLength(tokens) && + hasValidStructure(tokens) && + hasValidTokenOrder(tokens, allowIncomplete = true) + } + + /** + * ์‚ฐ์ˆ  ํ‘œํ˜„์‹์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ์‹œํ€€์Šค + * @return ์œ ํšจํ•œ ์‚ฐ์ˆ  ํ‘œํ˜„์‹์ด๋ฉด true + */ + fun isValidArithmeticExpression(tokens: List): Boolean { + if (!isSatisfiedBy(tokens)) return false + + return hasValidArithmeticStructure(tokens) && + hasValidOperatorOperandPattern(tokens) + } + + /** + * ๋…ผ๋ฆฌ ํ‘œํ˜„์‹์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ์‹œํ€€์Šค + * @return ์œ ํšจํ•œ ๋…ผ๋ฆฌ ํ‘œํ˜„์‹์ด๋ฉด true + */ + fun isValidLogicalExpression(tokens: List): Boolean { + if (!isSatisfiedBy(tokens)) return false + + return hasValidLogicalStructure(tokens) && + hasValidBooleanOperands(tokens) + } + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ์‹œํ€€์Šค + * @return ์œ ํšจํ•œ ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด๋ฉด true + */ + fun isValidFunctionCall(tokens: List): Boolean { + if (tokens.isEmpty()) return false + + return hasValidFunctionCallStructure(tokens) && + hasValidArgumentList(tokens) + } + + /** + * ์กฐ๊ฑด ํ‘œํ˜„์‹์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokens ๊ฒ€์ฆํ•  ํ† ํฐ ์‹œํ€€์Šค + * @return ์œ ํšจํ•œ ์กฐ๊ฑด ํ‘œํ˜„์‹์ด๋ฉด true + */ + fun isValidConditionalExpression(tokens: List): Boolean { + if (!isSatisfiedBy(tokens)) return false + + return hasValidConditionalStructure(tokens) && + hasValidConditionAndBranches(tokens) + } + + /** + * ํ† ํฐ ์‹œํ€€์Šค์˜ ๊ธธ์ด๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidLength(tokens: List): Boolean { + if (tokens.size > MAX_TOKEN_SEQUENCE_LENGTH) { + throw ParserException.tokenSequenceExceedsLimit( + count = tokens.size, + limit = MAX_TOKEN_SEQUENCE_LENGTH + ) + } + return true + } + + /** + * ๊ธฐ๋ณธ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidStructure(tokens: List): Boolean { + if (tokens.isEmpty()) return true + + // ์ฒซ ํ† ํฐ๊ณผ ๋งˆ์ง€๋ง‰ ํ† ํฐ ๊ฒ€์ฆ + val first = tokens.first() + val last = tokens.last() + + // ์—ฐ์‚ฐ์ž๋กœ ์‹œ์ž‘ํ•˜๋ฉด ์•ˆ๋จ (๋‹จํ•ญ ์—ฐ์‚ฐ์ž ์ œ์™ธ) + if (first.type.isOperator && !isUnaryOperator(first.type)) { + return false + } + + // ์ดํ•ญ ์—ฐ์‚ฐ์ž๋กœ ๋๋‚˜๋ฉด ์•ˆ๋จ + if (last.type.isOperator && isBinaryOperator(last.type)) { + return false + } + + return true + } + + /** + * ๊ตฌ๋ถ„์ž๊ฐ€ ๊ท ํ˜•์„ ์ด๋ฃจ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasBalancedDelimiters(tokens: List): Boolean { + var parenBalance = 0 + var braceBalance = 0 + + for (token in tokens) { + when (token.type) { + TokenType.LEFT_PAREN -> parenBalance++ + TokenType.RIGHT_PAREN -> { + parenBalance-- + if (parenBalance < 0) return false + } + // ํ•„์š”์‹œ ๋‹ค๋ฅธ ๊ตฌ๋ถ„์ž๋“ค ์ถ”๊ฐ€ + else -> { /* ๋ฌด์‹œ */ } + } + } + + return parenBalance == 0 && braceBalance == 0 + } + + /** + * ํ† ํฐ ์ˆœ์„œ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidTokenOrder(tokens: List, allowIncomplete: Boolean = false): Boolean { + if (tokens.isEmpty()) return true + + for (i in 0 until tokens.size - 1) { + val current = tokens[i] + val next = tokens[i + 1] + + if (!isValidTokenPair(current, next, allowIncomplete)) { + return false + } + } + + return true + } + + /** + * ์ค‘์ฒฉ ๊นŠ์ด๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidNestingDepth(tokens: List): Boolean { + var depth = 0 + var maxDepth = 0 + + for (token in tokens) { + when (token.type) { + TokenType.LEFT_PAREN -> { + depth++ + maxDepth = maxOf(maxDepth, depth) + } + TokenType.RIGHT_PAREN -> depth-- + else -> { /* ๋ฌด์‹œ */ } + } + + if (maxDepth > MAX_NESTING_DEPTH) { + throw ParserException.nestingDepthExceedsLimit( + depth = maxDepth, + limit = MAX_NESTING_DEPTH + ) + } + } + + return true + } + + /** + * ํ‘œํ˜„์‹ ๋ณต์žก๋„๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidExpressionComplexity(tokens: List): Boolean { + val operatorCount = tokens.count { it.type.isOperator } + val complexity = calculateComplexity(tokens) + + if (complexity > MAX_EXPRESSION_COMPLEXITY) { + throw ParserException.expressionComplexityExceedsLimit( + complexity = complexity, + limit = MAX_EXPRESSION_COMPLEXITY + ) + } + + return true + } + + /** + * ์‹œ์ž‘๊ณผ ๋์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidStartAndEnd(tokens: List): Boolean { + if (tokens.isEmpty()) return false + + val first = tokens.first() + val last = tokens.last() + + // ์œ ํšจํ•œ ์‹œ์ž‘ ํ† ํฐ๋“ค + val validStarts = setOf( + TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.VARIABLE, + TokenType.TRUE, TokenType.FALSE, TokenType.LEFT_PAREN, + TokenType.MINUS, TokenType.NOT // ๋‹จํ•ญ ์—ฐ์‚ฐ์ž๋“ค + ) + + // ์œ ํšจํ•œ ๋ ํ† ํฐ๋“ค + val validEnds = setOf( + TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.VARIABLE, + TokenType.TRUE, TokenType.FALSE, TokenType.RIGHT_PAREN + ) + + return first.type in validStarts && last.type in validEnds + } + + /** + * ๋ถˆ์™„์ „ํ•œ ์—ฐ์‚ฐ์ด ์—†๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasNoIncompleteOperations(tokens: List): Boolean { + // ์—ฐ์†๋œ ์—ฐ์‚ฐ์ž ๊ฒ€์‚ฌ + for (i in 0 until tokens.size - 1) { + val current = tokens[i] + val next = tokens[i + 1] + + if (current.type.isOperator && next.type.isOperator) { + // ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ๋‹ค์Œ์˜ ๊ฒฝ์šฐ๋Š” ํ—ˆ์šฉ + if (!isUnaryOperator(next.type)) { + return false + } + } + } + + return true + } + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidFunctionCalls(tokens: List): Boolean { + var i = 0 + while (i < tokens.size) { + if (tokens[i].type == TokenType.IDENTIFIER && + i + 1 < tokens.size && + tokens[i + 1].type == TokenType.LEFT_PAREN) { + + // ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ตฌ์กฐ ๊ฒ€์ฆ + val closeIndex = findMatchingParen(tokens, i + 1) + if (closeIndex == -1) return false + + val argsTokens = tokens.subList(i + 2, closeIndex) + if (!hasValidArgumentList(argsTokens)) return false + + i = closeIndex + 1 + } else { + i++ + } + } + + return true + } + + /** + * ์‚ฐ์ˆ  ํ‘œํ˜„์‹ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidArithmeticStructure(tokens: List): Boolean { + val arithmeticTokens = setOf( + TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.VARIABLE, + TokenType.PLUS, TokenType.MINUS, TokenType.MULTIPLY, + TokenType.DIVIDE, TokenType.POWER, TokenType.MODULO, + TokenType.LEFT_PAREN, TokenType.RIGHT_PAREN + ) + + return tokens.all { it.type in arithmeticTokens } + } + + /** + * ์—ฐ์‚ฐ์ž-ํ”ผ์—ฐ์‚ฐ์ž ํŒจํ„ด์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidOperatorOperandPattern(tokens: List): Boolean { + var expectingOperand = true // ์‹œ์ž‘์€ ํ”ผ์—ฐ์‚ฐ์ž๋ฅผ ๊ธฐ๋Œ€ + + for (token in tokens) { + when { + token.type in setOf(TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.VARIABLE) -> { + if (!expectingOperand) return false + expectingOperand = false + } + token.type in setOf(TokenType.PLUS, TokenType.MINUS, + TokenType.MULTIPLY, TokenType.DIVIDE, + TokenType.POWER, TokenType.MODULO) -> { + if (expectingOperand && !isUnaryOperator(token.type)) return false + expectingOperand = true + } + token.type == TokenType.LEFT_PAREN -> { + // ๊ด„ํ˜ธ๋Š” ํŒจํ„ด์„ ๋ณ€๊ฒฝํ•˜์ง€ ์•Š์Œ + } + token.type == TokenType.RIGHT_PAREN -> { + if (expectingOperand) return false + expectingOperand = false + } + } + } + + return !expectingOperand // ๋งˆ์ง€๋ง‰์€ ํ”ผ์—ฐ์‚ฐ์ž์—ฌ์•ผ ํ•จ + } + + /** + * ๋…ผ๋ฆฌ ํ‘œํ˜„์‹ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidLogicalStructure(tokens: List): Boolean { + val logicalTokens = setOf( + TokenType.TRUE, TokenType.FALSE, TokenType.IDENTIFIER, TokenType.VARIABLE, + TokenType.AND, TokenType.OR, TokenType.NOT, + TokenType.EQUAL, TokenType.NOT_EQUAL, + TokenType.LESS, TokenType.LESS_EQUAL, + TokenType.GREATER, TokenType.GREATER_EQUAL, + TokenType.LEFT_PAREN, TokenType.RIGHT_PAREN + ) + + return tokens.any { it.type in logicalTokens } + } + + /** + * ๋ถˆ๋ฆฐ ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidBooleanOperands(tokens: List): Boolean { + // ๊ฐ„๋‹จํ•œ ๊ฒ€์ฆ: ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž ์•ž๋’ค์— ์ ์ ˆํ•œ ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ + for (i in tokens.indices) { + val token = tokens[i] + if (token.type in setOf(TokenType.AND, TokenType.OR)) { + // ์•ž๋’ค๋กœ ์œ ํšจํ•œ ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ + val hasPrevOperand = i > 0 && isValidLogicalOperand(tokens[i - 1]) + val hasNextOperand = i < tokens.size - 1 && isValidLogicalOperand(tokens[i + 1]) + + if (!hasPrevOperand || !hasNextOperand) return false + } + } + + return true + } + + /** + * ํ•จ์ˆ˜ ํ˜ธ์ถœ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidFunctionCallStructure(tokens: List): Boolean { + if (tokens.size < 3) return false // ์ตœ์†Œ: identifier ( ) + + return tokens[0].type == TokenType.IDENTIFIER && + tokens[1].type == TokenType.LEFT_PAREN && + tokens.last().type == TokenType.RIGHT_PAREN + } + + /** + * ์ธ์ˆ˜ ๋ชฉ๋ก์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidArgumentList(tokens: List): Boolean { + if (tokens.isEmpty()) return true // ๋นˆ ์ธ์ˆ˜ ๋ชฉ๋ก์€ ์œ ํšจ + + var expectingArg = true + for (token in tokens) { + when (token.type) { + TokenType.COMMA -> { + if (expectingArg) return false + expectingArg = true + } + in setOf(TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.VARIABLE, + TokenType.TRUE, TokenType.FALSE) -> { + if (!expectingArg) return false + expectingArg = false + } + TokenType.LEFT_PAREN -> { /* ์ค‘์ฒฉ ํ•จ์ˆ˜ ํ˜ธ์ถœ ํ—ˆ์šฉ */ } + TokenType.RIGHT_PAREN -> { /* ์ค‘์ฒฉ ํ•จ์ˆ˜ ํ˜ธ์ถœ ํ—ˆ์šฉ */ } + else -> { /* ignore other tokens for argument validation */ } + } + } + + return !expectingArg // ๋งˆ์ง€๋ง‰์€ ์ธ์ˆ˜์—ฌ์•ผ ํ•จ + } + + /** + * ์กฐ๊ฑด ํ‘œํ˜„์‹ ๊ตฌ์กฐ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidConditionalStructure(tokens: List): Boolean { + return tokens.any { it.type == TokenType.IF } + } + + /** + * ์กฐ๊ฑด๊ณผ ๋ถ„๊ธฐ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun hasValidConditionAndBranches(tokens: List): Boolean { + // IF ( condition , true_branch , false_branch ) ๊ตฌ์กฐ ๊ฒ€์ฆ + val ifIndex = tokens.indexOfFirst { it.type == TokenType.IF } + if (ifIndex == -1) return false + + if (ifIndex + 1 >= tokens.size || tokens[ifIndex + 1].type != TokenType.LEFT_PAREN) { + return false + } + + val closeIndex = findMatchingParen(tokens, ifIndex + 1) + if (closeIndex == -1) return false + + val innerTokens = tokens.subList(ifIndex + 2, closeIndex) + val commaIndices = innerTokens.mapIndexedNotNull { index, token -> + if (token.type == TokenType.COMMA) index else null + } + + // ์ •ํ™•ํžˆ 2๊ฐœ์˜ ์‰ผํ‘œ๊ฐ€ ์žˆ์–ด์•ผ ํ•จ (3๊ฐœ ๋ถ€๋ถ„์œผ๋กœ ๋‚˜๋‰จ) + return commaIndices.size == 2 + } + + // Helper methods + + private fun isUnaryOperator(type: TokenType): Boolean { + return type in setOf(TokenType.MINUS, TokenType.NOT, TokenType.PLUS) + } + + private fun isBinaryOperator(type: TokenType): Boolean { + return type.isOperator && !isUnaryOperator(type) + } + + private fun isValidTokenPair(current: Token, next: Token, allowIncomplete: Boolean): Boolean { + // ๊ธฐ๋ณธ์ ์ธ ํ† ํฐ ์Œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + val currentType = current.type + val nextType = next.type + + // ์—ฐ์†๋œ ํ”ผ์—ฐ์‚ฐ์ž๋Š” ํ—ˆ์šฉํ•˜์ง€ ์•Š์Œ + if (isOperand(currentType) && isOperand(nextType)) { + return false + } + + // ์ดํ•ญ ์—ฐ์‚ฐ์ž ๋’ค์—๋Š” ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์™€์•ผ ํ•จ + if (isBinaryOperator(currentType) && !isOperand(nextType) && nextType != TokenType.LEFT_PAREN) { + return allowIncomplete + } + + return true + } + + private fun isOperand(type: TokenType): Boolean { + return type in setOf( + TokenType.NUMBER, TokenType.IDENTIFIER, TokenType.VARIABLE, + TokenType.TRUE, TokenType.FALSE + ) + } + + private fun isValidLogicalOperand(token: Token): Boolean { + return token.type in setOf( + TokenType.TRUE, TokenType.FALSE, TokenType.IDENTIFIER, + TokenType.VARIABLE, TokenType.RIGHT_PAREN + ) + } + + private fun findMatchingParen(tokens: List, startIndex: Int): Int { + var depth = 1 + for (i in startIndex + 1 until tokens.size) { + when (tokens[i].type) { + TokenType.LEFT_PAREN -> depth++ + TokenType.RIGHT_PAREN -> { + depth-- + if (depth == 0) return i + } + else -> { /* ignore other tokens */ } + } + } + return -1 + } + + private fun calculateComplexity(tokens: List): Int { + var complexity = 0 + var nestingLevel = 0 + + for (token in tokens) { + when (token.type) { + TokenType.LEFT_PAREN -> { + nestingLevel++ + complexity += nestingLevel + } + TokenType.RIGHT_PAREN -> nestingLevel-- + in setOf(TokenType.PLUS, TokenType.MINUS, TokenType.MULTIPLY, + TokenType.DIVIDE, TokenType.POWER, TokenType.MODULO) -> { + complexity += 1 + nestingLevel + } + in setOf(TokenType.AND, TokenType.OR) -> { + complexity += 2 + nestingLevel + } + TokenType.IF -> complexity += 5 + nestingLevel + else -> { /* ignore other tokens for complexity calculation */ } + } + } + + return complexity + } + + /** + * ๋ช…์„ธ์˜ ์„ค์ • ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ค์ • ์ •๋ณด ๋งต + */ + fun getSpecificationInfo(): Map = mapOf( + "name" to ParsingValiditySpecConstants.NAME, + "maxTokenSequenceLength" to ParsingValiditySpecConstants.MAX_TOKEN_SEQUENCE_LENGTH, + "maxNestingDepth" to ParsingValiditySpecConstants.MAX_NESTING_DEPTH, + "maxExpressionComplexity" to ParsingValiditySpecConstants.MAX_EXPRESSION_COMPLEXITY, + "supportedValidations" to ParsingValiditySpecConstants.SUPPORTED_VALIDATIONS + ) + + object ParsingValiditySpecConstants { + const val NAME = "ParsingValiditySpec" + const val MAX_TOKEN_SEQUENCE_LENGTH = 200 // ๊ธฐ์กด ๊ฐ’ ์œ ์ง€ + const val MAX_NESTING_DEPTH = 50 + const val MAX_EXPRESSION_COMPLEXITY = 1000 + + val SUPPORTED_VALIDATIONS = listOf( + "length", + "structure", + "balancedDelimiters", + "tokenOrder", + "nestingDepth", + "expressionComplexity", + "completeness", + "arithmeticExpression", + "logicalExpression", + "functionCall", + "conditionalExpression" + ) + } + +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/Associativity.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/Associativity.kt new file mode 100644 index 00000000..fea556a4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/Associativity.kt @@ -0,0 +1,406 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.exceptions.ParserException + +/** + * ์—ฐ์‚ฐ์ž์˜ ๊ฒฐํ•ฉ์„ฑ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ๋™์ผํ•œ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง„ ์—ฐ์‚ฐ์ž๋“ค์ด ์—ฐ์†์œผ๋กœ ๋‚˜ํƒ€๋‚  ๋•Œ + * ์–ด๋–ค ๋ฐฉํ–ฅ์œผ๋กœ ๊ฒฐํ•ฉํ• ์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๊ทœ์น™์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * DDD Value Object ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๋ถˆ๋ณ€์„ฑ๊ณผ ๋„๋ฉ”์ธ ์˜๋ฏธ๋ฅผ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @property type ๊ฒฐํ•ฉ์„ฑ ํƒ€์ž… + * @property operator ์—ฐ์‚ฐ์ž ํ† ํฐ ํƒ€์ž… + * @property precedence ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ์šฐ์„ ) + * @property description ๊ฒฐํ•ฉ์„ฑ ์„ค๋ช… + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +data class Associativity( + val type: AssociativityType, + val operator: TokenType, + val precedence: Int, + val description: String = "" +) { + + init { + if (!(operator.isOperator || operator.isTerminal)) { + throw ParserException.operatorTokenRequired(operator) + } + + if (precedence < 0) { + throw ParserException.precedenceNegative(precedence) + } + } + + /** + * ๊ฒฐํ•ฉ์„ฑ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class AssociativityType(val symbol: String, val description: String) { + /** ์ขŒ๊ฒฐํ•ฉ: a op b op c = (a op b) op c */ + LEFT("L", "์ขŒ๊ฒฐํ•ฉ"), + + /** ์šฐ๊ฒฐํ•ฉ: a op b op c = a op (b op c) */ + RIGHT("R", "์šฐ๊ฒฐํ•ฉ"), + + /** ๋น„๊ฒฐํ•ฉ: a op b op c = ์˜ค๋ฅ˜ */ + NONE("N", "๋น„๊ฒฐํ•ฉ"), + + /** ์ฒด์ธ๊ฒฐํ•ฉ: a op b op c = a op b && b op c (๋น„๊ต ์—ฐ์‚ฐ์ž์šฉ) */ + CHAIN("C", "์ฒด์ธ๊ฒฐํ•ฉ"); + + /** + * ์ขŒ๊ฒฐํ•ฉ ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ขŒ๊ฒฐํ•ฉ์ด๋ฉด true + */ + fun isLeft(): Boolean = this == LEFT + + /** + * ์šฐ๊ฒฐํ•ฉ ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ๊ฒฐํ•ฉ์ด๋ฉด true + */ + fun isRight(): Boolean = this == RIGHT + + /** + * ๋น„๊ฒฐํ•ฉ ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋น„๊ฒฐํ•ฉ์ด๋ฉด true + */ + fun isNone(): Boolean = this == NONE + + /** + * ์ฒด์ธ๊ฒฐํ•ฉ ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฒด์ธ๊ฒฐํ•ฉ์ด๋ฉด true + */ + fun isChain(): Boolean = this == CHAIN + + companion object { + /** + * ์‹ฌ๋ณผ๋กœ๋ถ€ํ„ฐ ๊ฒฐํ•ฉ์„ฑ ํƒ€์ž…์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + * + * @param symbol ๊ฒฐํ•ฉ์„ฑ ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ๊ฒฐํ•ฉ์„ฑ ํƒ€์ž… + * @throws IllegalArgumentException ์•Œ ์ˆ˜ ์—†๋Š” ์‹ฌ๋ณผ์ธ ๊ฒฝ์šฐ + */ + fun fromSymbol(symbol: String): AssociativityType { + return values().find { it.symbol == symbol } + ?: throw ParserException.unknownAssociativitySymbol(symbol) + } + } + } + + companion object { + /** + * ๊ณตํ†ต Associativity ์ƒ์„ฑ ๋กœ์ง์ž…๋‹ˆ๋‹ค. + * + * @param type ๊ฒฐํ•ฉ์„ฑ ํƒ€์ž… + * @param operator ์—ฐ์‚ฐ์ž ํ† ํฐ + * @param precedence ์šฐ์„ ์ˆœ์œ„ + * @param description ์„ค๋ช… + * @return ์ƒ์„ฑ๋œ Associativity + */ + private fun create( + type: AssociativityType, + operator: TokenType, + precedence: Int, + description: String = "" + ): Associativity { + return Associativity(type, operator, precedence, description) + } + + /** + * ์ขŒ๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ํ† ํฐ + * @param precedence ์šฐ์„ ์ˆœ์œ„ + * @param description ์„ค๋ช… + * @return ์ขŒ๊ฒฐํ•ฉ Associativity + */ + fun leftAssoc( + operator: TokenType, + precedence: Int, + description: String = "" + ): Associativity = create(AssociativityType.LEFT, operator, precedence, description) + + /** + * ์šฐ๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ํ† ํฐ + * @param precedence ์šฐ์„ ์ˆœ์œ„ + * @param description ์„ค๋ช… + * @return ์šฐ๊ฒฐํ•ฉ Associativity + */ + fun rightAssoc( + operator: TokenType, + precedence: Int, + description: String = "" + ): Associativity = create(AssociativityType.RIGHT, operator, precedence, description) + + /** + * ๋น„๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ํ† ํฐ + * @param precedence ์šฐ์„ ์ˆœ์œ„ + * @param description ์„ค๋ช… + * @return ๋น„๊ฒฐํ•ฉ Associativity + */ + fun nonAssoc( + operator: TokenType, + precedence: Int, + description: String = "" + ): Associativity = create(AssociativityType.NONE, operator, precedence, description) + + /** + * ์ฒด์ธ๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param operator ์—ฐ์‚ฐ์ž ํ† ํฐ + * @param precedence ์šฐ์„ ์ˆœ์œ„ + * @param description ์„ค๋ช… + * @return ์ฒด์ธ๊ฒฐํ•ฉ Associativity + */ + fun chainAssoc( + operator: TokenType, + precedence: Int, + description: String = "" + ): Associativity = create(AssociativityType.CHAIN, operator, precedence, description) + + /** + * ๊ธฐ๋ณธ ์—ฐ์‚ฐ์ž๋“ค์˜ ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™๋“ค + */ + fun getDefaultRules(): List = listOf( + // ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž (๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„) + leftAssoc(TokenType.OR, 1, "๋…ผ๋ฆฌํ•ฉ ์—ฐ์‚ฐ์ž"), + leftAssoc(TokenType.AND, 2, "๋…ผ๋ฆฌ๊ณฑ ์—ฐ์‚ฐ์ž"), + + // ๋น„๊ต ์—ฐ์‚ฐ์ž + chainAssoc(TokenType.EQUAL, 3, "๊ฐ™์Œ ๋น„๊ต"), + chainAssoc(TokenType.NOT_EQUAL, 3, "๋‹ค๋ฆ„ ๋น„๊ต"), + chainAssoc(TokenType.LESS, 4, "๋ฏธ๋งŒ ๋น„๊ต"), + chainAssoc(TokenType.LESS_EQUAL, 4, "์ดํ•˜ ๋น„๊ต"), + chainAssoc(TokenType.GREATER, 4, "์ดˆ๊ณผ ๋น„๊ต"), + chainAssoc(TokenType.GREATER_EQUAL, 4, "์ด์ƒ ๋น„๊ต"), + + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž + leftAssoc(TokenType.PLUS, 5, "๋ง์…ˆ"), + leftAssoc(TokenType.MINUS, 5, "๋บ„์…ˆ"), + leftAssoc(TokenType.MULTIPLY, 6, "๊ณฑ์…ˆ"), + leftAssoc(TokenType.DIVIDE, 6, "๋‚˜๋ˆ—์…ˆ"), + leftAssoc(TokenType.MODULO, 6, "๋‚˜๋จธ์ง€"), + + // ์ง€์ˆ˜ ์—ฐ์‚ฐ์ž (๋†’์€ ์šฐ์„ ์ˆœ์œ„, ์šฐ๊ฒฐํ•ฉ) + rightAssoc(TokenType.POWER, 7, "๊ฑฐ๋“ญ์ œ๊ณฑ"), + + // ๋‹จํ•ญ ์—ฐ์‚ฐ์ž๋“ค (๊ฐ€์žฅ ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + rightAssoc(TokenType.NOT, 8, "๋…ผ๋ฆฌ ๋ถ€์ •"), + rightAssoc(TokenType.MINUS, 8, "๋‹จํ•ญ ๋งˆ์ด๋„ˆ์Šค"), // ๋ฌธ๋งฅ์— ๋”ฐ๋ผ ์ดํ•ญ/๋‹จํ•ญ ๊ตฌ๋ถ„ ํ•„์š” + rightAssoc(TokenType.PLUS, 8, "๋‹จํ•ญ ํ”Œ๋Ÿฌ์Šค") + ) + + /** + * ์—ฐ์‚ฐ์ž๋ณ„ ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™ ๋งต์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฐ์‚ฐ์ž -> ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™ ๋งต + */ + fun getDefaultRuleMap(): Map { + return getDefaultRules().associateBy { it.operator } + } + } + + /** + * ์ขŒ๊ฒฐํ•ฉ์ธ์ง€ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + */ + val isLeftAssociative: Boolean get() = type == AssociativityType.LEFT + + /** + * ์šฐ๊ฒฐํ•ฉ์ธ์ง€ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + */ + val isRightAssociative: Boolean get() = type == AssociativityType.RIGHT + + /** + * ๋น„๊ฒฐํ•ฉ์ธ์ง€ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + */ + val isNonAssociative: Boolean get() = type == AssociativityType.NONE + + /** + * ์ฒด์ธ๊ฒฐํ•ฉ์ธ์ง€ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + */ + val isChainAssociative: Boolean get() = type == AssociativityType.CHAIN + + /** + * ๋‹ค๋ฅธ ์—ฐ์‚ฐ์ž์™€์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋น„๊ตํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™ + * @return ์ด ๊ทœ์น™์ด ๋” ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋ฉด ์–‘์ˆ˜, ๊ฐ™์œผ๋ฉด 0, ๋‚ฎ์œผ๋ฉด ์Œ์ˆ˜ + */ + fun comparePrecedence(other: Associativity): Int { + return this.precedence.compareTo(other.precedence) + } + + /** + * ๋™์ผํ•œ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™ + * @return ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๊ฐ™์œผ๋ฉด true + */ + fun hasSamePrecedence(other: Associativity): Boolean { + return this.precedence == other.precedence + } + + /** + * ๋” ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™ + * @return ๋” ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋ฉด true + */ + fun hasHigherPrecedence(other: Associativity): Boolean { + return this.precedence > other.precedence + } + + /** + * ๋” ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™ + * @return ๋” ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„๋ฉด true + */ + fun hasLowerPrecedence(other: Associativity): Boolean { + return this.precedence < other.precedence + } + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + * ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋จผ์ € ๋น„๊ตํ•˜๊ณ , ๋™์ผํ•  ๊ฒฝ์šฐ ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ์ถฉ๋Œํ•˜๋Š” ๋‹ค๋ฅธ ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™ + * @return ์ถฉ๋Œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + */ + fun resolveConflict(other: Associativity): ConflictResolution { + return when { + precedence > other.precedence -> ConflictResolution.SHIFT + precedence < other.precedence -> ConflictResolution.REDUCE + else -> resolveSamePrecedenceConflict(other) + } + } + + /** + * ๋™์ผํ•œ ์šฐ์„ ์ˆœ์œ„์—์„œ์˜ ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•ฉ๋‹ˆ๋‹ค. + * ๊ฒฐํ•ฉ์„ฑ ํƒ€์ž…์— ๋”ฐ๋ผ ์ ์ ˆํ•œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ์ถฉ๋Œํ•˜๋Š” ๋‹ค๋ฅธ ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™ + * @return ์ถฉ๋Œ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ• + */ + private fun resolveSamePrecedenceConflict(other: Associativity): ConflictResolution = when { + isLeftAssociative && other.isLeftAssociative -> ConflictResolution.REDUCE + isRightAssociative && other.isRightAssociative -> ConflictResolution.SHIFT + isNonAssociative || other.isNonAssociative -> ConflictResolution.ERROR + isChainAssociative && other.isChainAssociative -> ConflictResolution.SPECIAL + else -> ConflictResolution.ERROR + } + + /** + * ์ถฉ๋Œ ํ•ด๊ฒฐ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ + enum class ConflictResolution(val description: String) { + SHIFT("์‹œํ”„ํŠธ ์ˆ˜ํ–‰"), + REDUCE("๋ฆฌ๋“€์Šค ์ˆ˜ํ–‰"), + ERROR("์—๋Ÿฌ ๋ฐœ์ƒ"), + SPECIAL("ํŠน์ˆ˜ ์ฒ˜๋ฆฌ ํ•„์š”") + } + + /** + * ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true + */ + fun isValid(): Boolean { + return try { + // ์—ฐ์‚ฐ์ž๊ฐ€ ์‹ค์ œ ์—ฐ์‚ฐ์ž ํ† ํฐ์ธ์ง€ ํ™•์ธ + operator.isOperator || operator.isTerminal + } catch (e: Exception) { + false + } + } + + /** + * ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newPrecedence ์ƒˆ๋กœ์šด ์šฐ์„ ์ˆœ์œ„ + * @return ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋ณ€๊ฒฝ๋œ ์ƒˆ ๊ทœ์น™ + */ + fun withPrecedence(newPrecedence: Int): Associativity { + return copy(precedence = newPrecedence) + } + + /** + * ์„ค๋ช…์„ ๋ณ€๊ฒฝํ•œ ์ƒˆ๋กœ์šด ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param newDescription ์ƒˆ๋กœ์šด ์„ค๋ช… + * @return ์„ค๋ช…์ด ๋ณ€๊ฒฝ๋œ ์ƒˆ ๊ทœ์น™ + */ + fun withDescription(newDescription: String): Associativity { + return copy(description = newDescription) + } + + /** + * ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ์ •๋ณด ๋งต + */ + fun getDetailInfo(): Map = mapOf( + "operator" to operator.name, + "associativity" to type.description, + "precedence" to precedence, + "description" to description, + "isLeftAssoc" to isLeftAssociative, + "isRightAssoc" to isRightAssociative, + "isNonAssoc" to isNonAssociative, + "isChainAssoc" to isChainAssociative, + "symbol" to type.symbol, + "isValid" to isValid() + ) + + /** + * ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์„ ํ…Œ์ด๋ธ” ํ˜•ํƒœ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ…Œ์ด๋ธ” ๋ฌธ์ž์—ด + */ + fun toTableRow(): String { + return "%-12s | %-8s | %-10d | %s".format( + operator.name, + type.symbol, + precedence, + description.take(30) + ) + } + + /** + * ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์„ ์ƒ์„ธ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ์ •๋ณด ๋ฌธ์ž์—ด + */ + fun toDetailString(): String = buildString { + append("$operator: ") + append("${type.description}(${type.symbol}), ") + append("์šฐ์„ ์ˆœ์œ„=$precedence") + if (description.isNotEmpty()) { + append(", $description") + } + } + + /** + * ๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์˜ ๊ฐ„๋‹จํ•œ ์š”์•ฝ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์š”์•ฝ ๋ฌธ์ž์—ด + */ + override fun toString(): String { + return "$operator(${type.symbol},$precedence)" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/FirstFollowSets.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/FirstFollowSets.kt new file mode 100644 index 00000000..99bbd8b1 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/FirstFollowSets.kt @@ -0,0 +1,259 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.Production + +/** + * LR(1) ํŒŒ์„œ ํ…Œ์ด๋ธ” ๊ตฌ์ถ•์— ํ•„์ˆ˜์ ์ธ FIRST ๋ฐ FOLLOW ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•˜๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * FIRST/FOLLOW ์ง‘ํ•ฉ์€ LR(1) ํŒŒ์‹ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์˜ ํ•ต์‹ฌ ๊ตฌ์„ฑ์š”์†Œ๋กœ, + * ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์˜ ์•ก์…˜๊ณผ ์ถฉ๋Œ ํ•ด๊ฒฐ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ FirstFollowSets ๊ฐ์ฒด๋ฅผ DDD ๊ตฌ์กฐ๋กœ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @property firstSets ๊ฐ ์‹ฌ๋ณผ์˜ FIRST ์ง‘ํ•ฉ + * @property followSets ๊ฐ ์‹ฌ๋ณผ์˜ FOLLOW ์ง‘ํ•ฉ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +class FirstFollowSets private constructor( + private val firstSets: Map>, + private val followSets: Map> +) { + + /** + * ์ฃผ์–ด์ง„ ์‹ฌ๋ณผ์˜ FIRST ์ง‘ํ•ฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol FIRST ์ง‘ํ•ฉ์„ ์กฐํšŒํ•  ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ์‹ฌ๋ณผ์˜ FIRST ์ง‘ํ•ฉ + */ + fun getFirst(symbol: TokenType): Set = firstSets[symbol] ?: emptySet() + + /** + * ์ฃผ์–ด์ง„ ์‹ฌ๋ณผ์˜ FOLLOW ์ง‘ํ•ฉ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbol FOLLOW ์ง‘ํ•ฉ์„ ์กฐํšŒํ•  ์‹ฌ๋ณผ + * @return ํ•ด๋‹น ์‹ฌ๋ณผ์˜ FOLLOW ์ง‘ํ•ฉ + */ + fun getFollow(symbol: TokenType): Set = followSets[symbol] ?: emptySet() + + /** + * ์‹ฌ๋ณผ ์‹œํ€€์Šค์˜ FIRST ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * companion object์˜ ์ •์  ๋ฉ”์„œ๋“œ์— ์œ„์ž„ํ•˜์—ฌ ์ฝ”๋“œ ์ค‘๋ณต์„ ๋ฐฉ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param symbols FIRST ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•  ์‹ฌ๋ณผ ์‹œํ€€์Šค + * @return ์‹ฌ๋ณผ ์‹œํ€€์Šค์˜ FIRST ์ง‘ํ•ฉ + */ + fun firstOfSequence(symbols: List): Set { + return firstOfSequence(symbols, firstSets) + } + + + companion object { + /** + * ์ฃผ์–ด์ง„ ๋ฌธ๋ฒ• ์ •๋ณด๋กœ๋ถ€ํ„ฐ FIRST/FOLLOW ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•˜์—ฌ FirstFollowSets๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param productions ๋ฌธ๋ฒ•์˜ ์ƒ์‚ฐ ๊ทœ์น™ ๋ชฉ๋ก + * @param terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @param nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @param startSymbol ์‹œ์ž‘ ์‹ฌ๋ณผ + * @return ๊ณ„์‚ฐ๋œ FirstFollowSets ์ธ์Šคํ„ด์Šค + */ + fun compute( + productions: List, + terminals: Set, + nonTerminals: Set, + startSymbol: TokenType + ): FirstFollowSets { + val firstSets = mutableMapOf>() + val followSets = mutableMapOf>() + + // FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ + calculateFirstSets(firstSets, productions, terminals, nonTerminals) + + // FOLLOW ์ง‘ํ•ฉ ๊ณ„์‚ฐ + calculateFollowSets(followSets, firstSets, productions, nonTerminals, startSymbol) + + return FirstFollowSets( + firstSets.mapValues { it.value.toSet() }, + followSets.mapValues { it.value.toSet() } + ) + } + + /** + * ๋ฌธ๋ฒ•์˜ ๋ชจ๋“  ์‹ฌ๋ณผ์— ๋Œ€ํ•œ FIRST ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun calculateFirstSets( + firstSets: MutableMap>, + productions: List, + terminals: Set, + nonTerminals: Set + ) { + initializeFirstSets(firstSets, terminals, nonTerminals) + applyFixedPoint { applyFirstSetRules(productions, firstSets) } + } + + /** + * FIRST ์ง‘ํ•ฉ์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun initializeFirstSets( + firstSets: MutableMap>, + terminals: Set, + nonTerminals: Set + ) { + // ๋ชจ๋“  ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์˜ FIRST ์ง‘ํ•ฉ์€ ์ž๊ธฐ ์ž์‹  + terminals.forEach { terminal -> + firstSets[terminal] = mutableSetOf(terminal) + } + + // ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์˜ FIRST ์ง‘ํ•ฉ์€ ์ดˆ๊ธฐ์— ๋น„์–ด ์žˆ์Œ + nonTerminals.forEach { firstSets[it] = mutableSetOf() } + } + + /** + * FIRST ์ง‘ํ•ฉ ๊ทœ์น™์„ ์ ์šฉํ•˜๊ณ  ๋ณ€๊ฒฝ์ด ์žˆ์—ˆ๋Š”์ง€ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun applyFirstSetRules( + productions: List, + firstSets: MutableMap> + ): Boolean { + var changed = false + for (production in productions) { + val currentFirstSet = firstSets[production.left] + if (currentFirstSet != null) { + val before = currentFirstSet.size + val firstOfRight = firstOfSequence(production.right, firstSets) + currentFirstSet.addAll(firstOfRight) + if (currentFirstSet.size > before) { + changed = true + } + } + } + return changed + } + + /** + * ๊ณ ์ •์ ์— ๋„๋‹ฌํ•  ๋•Œ๊นŒ์ง€ ๊ทœ์น™์„ ๋ฐ˜๋ณต ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + */ + private fun applyFixedPoint(applyRules: () -> Boolean) { + while (applyRules()) { + // ๋ณ€๊ฒฝ์ด ์—†์„ ๋•Œ๊นŒ์ง€ ๋ฐ˜๋ณต + } + } + + /** + * ๋ฌธ๋ฒ•์˜ ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์— ๋Œ€ํ•œ FOLLOW ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + private fun calculateFollowSets( + followSets: MutableMap>, + firstSets: Map>, + productions: List, + nonTerminals: Set, + startSymbol: TokenType + ) { + initializeFollowSets(followSets, nonTerminals, startSymbol) + applyFixedPoint { applyFollowSetRules(productions, followSets, firstSets, nonTerminals) } + } + + /** + * FOLLOW ์ง‘ํ•ฉ์„ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun initializeFollowSets( + followSets: MutableMap>, + nonTerminals: Set, + startSymbol: TokenType + ) { + // ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์˜ FOLLOW ์ง‘ํ•ฉ์€ ์ดˆ๊ธฐ์— ๋น„์–ด ์žˆ์Œ + nonTerminals.forEach { followSets[it] = mutableSetOf() } + + // ์‹œ์ž‘ ์‹ฌ๋ณผ์˜ FOLLOW ์ง‘ํ•ฉ์—๋Š” EOF($)๊ฐ€ ํฌํ•จ + followSets[startSymbol]?.add(TokenType.DOLLAR) + } + + /** + * FOLLOW ์ง‘ํ•ฉ ๊ทœ์น™์„ ์ ์šฉํ•˜๊ณ  ๋ณ€๊ฒฝ์ด ์žˆ์—ˆ๋Š”์ง€ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + private fun applyFollowSetRules( + productions: List, + followSets: MutableMap>, + firstSets: Map>, + nonTerminals: Set + ): Boolean { + var changed = false + for (production in productions) { + for (i in production.right.indices) { + val symbol = production.right[i] + if (symbol in nonTerminals) { + val currentFollowSet = followSets[symbol] + val productionFollowSet = followSets[production.left] + + if (currentFollowSet != null && productionFollowSet != null) { + val before = currentFollowSet.size + val beta = production.right.drop(i + 1) + val firstOfBeta = firstOfSequence(beta, firstSets) + currentFollowSet.addAll(firstOfBeta - TokenType.EPSILON) + + if (beta.isEmpty() || canDeriveEmpty(beta, firstSets)) { + currentFollowSet.addAll(productionFollowSet) + } + + if (currentFollowSet.size > before) { + changed = true + } + } + } + } + } + return changed + } + + /** + * ์‹ฌ๋ณผ ์‹œํ€€์Šค์˜ FIRST ์ง‘ํ•ฉ์„ ๊ณ„์‚ฐํ•˜๋Š” ํ—ฌํผ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + */ + private fun firstOfSequence( + symbols: List, + firstSets: Map> + ): Set { + if (symbols.isEmpty()) { + return setOf() + } + + val result = mutableSetOf() + var derivesEmpty = true + + for (symbol in symbols) { + val firstOfSymbol = firstSets[symbol] ?: setOf() + result.addAll(firstOfSymbol - TokenType.EPSILON) + if (TokenType.EPSILON !in firstOfSymbol) { + derivesEmpty = false + break + } + } + + if (derivesEmpty) { + result.add(TokenType.EPSILON) + } + + return result + } + + /** + * ์‹ฌ๋ณผ ์‹œํ€€์Šค๊ฐ€ epsilon์„ ํŒŒ์ƒํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun canDeriveEmpty( + symbols: List, + firstSets: Map> + ): Boolean { + return symbols.all { + TokenType.EPSILON in (firstSets[it] ?: emptySet()) + } + } + } + + override fun toString(): String { + return "FirstFollowSets(firstSets=${firstSets.size}, followSets=${followSets.size})" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/Grammar.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/Grammar.kt new file mode 100644 index 00000000..ca2b334c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/Grammar.kt @@ -0,0 +1,339 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.ast.factory.ASTBuilders +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate + +/** + * Grammar์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ƒ์ˆ˜ ๋ชจ์Œ + */ + +/** + * ๊ณ„์‚ฐ๊ธฐ ์–ธ์–ด์˜ ๋ฌธ๋ฒ• ๊ทœ์น™์„ ์ •์˜ํ•˜๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ์ž…๋‹ˆ๋‹ค. + * + * ๋ชจ๋“  ์ƒ์„ฑ ๊ทœ์น™(Production), ์‹œ์ž‘ ์‹ฌ๋ณผ, ํ™•์žฅ๋œ ์ƒ์„ฑ ๊ทœ์น™, ํ„ฐ๋ฏธ๋„ ๋ฐ ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * LR(1) ํŒŒ์„œ ํ…Œ์ด๋ธ” ๊ตฌ์ถ•์˜ ๊ธฐ๋ฐ˜์ด ๋˜๋Š” ์™„์ „ํ•œ BNF ๋ฌธ๋ฒ•์„ ์ œ๊ณตํ•˜๋ฉฐ, + * POC ์ฝ”๋“œ์˜ ๋ชจ๋“  ์—ฐ์‚ฐ์ž์™€ ๊ตฌ๋ฌธ์„ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * @author kangeunchan + * @since 2025.07.15 + */ +@Aggregate(context = "parser") +object Grammar { + + /** + * ๋ชจ๋“  ์ƒ์„ฑ ๊ทœ์น™ ๋ชฉ๋ก (AST ๋นŒ๋” ํฌํ•จ) + */ + val productions: List = listOf( + // ๋…ผ๋ฆฌํ•ฉ + Production(0, TokenType.EXPR, listOf(TokenType.EXPR, TokenType.OR, TokenType.AND_EXPR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_OR)), + Production(1, TokenType.EXPR, listOf(TokenType.AND_EXPR), ASTBuilders.Identity), + + // ๋…ผ๋ฆฌ๊ณฑ + Production(2, TokenType.AND_EXPR, listOf(TokenType.AND_EXPR, TokenType.AND, TokenType.COMP_EXPR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_AND)), + Production(3, TokenType.AND_EXPR, listOf(TokenType.COMP_EXPR), ASTBuilders.Identity), + + // ๋น„๊ต ์—ฐ์‚ฐ + Production(4, TokenType.COMP_EXPR, listOf(TokenType.COMP_EXPR, TokenType.EQUAL, TokenType.ARITH_EXPR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_EQ)), + Production(5, TokenType.COMP_EXPR, listOf(TokenType.COMP_EXPR, TokenType.NOT_EQUAL, TokenType.ARITH_EXPR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_NEQ)), + Production(6, TokenType.COMP_EXPR, listOf(TokenType.COMP_EXPR, TokenType.LESS, TokenType.ARITH_EXPR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_LT)), + Production(7, TokenType.COMP_EXPR, listOf(TokenType.COMP_EXPR, TokenType.LESS_EQUAL, TokenType.ARITH_EXPR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_LTE)), + Production(8, TokenType.COMP_EXPR, listOf(TokenType.COMP_EXPR, TokenType.GREATER, TokenType.ARITH_EXPR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_GT)), + Production(9, TokenType.COMP_EXPR, listOf(TokenType.COMP_EXPR, TokenType.GREATER_EQUAL, TokenType.ARITH_EXPR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_GTE)), + Production(10, TokenType.COMP_EXPR, listOf(TokenType.ARITH_EXPR), ASTBuilders.Identity), + + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ + Production(11, TokenType.ARITH_EXPR, listOf(TokenType.ARITH_EXPR, TokenType.PLUS, TokenType.TERM), + ASTBuilders.createBinaryOp(GrammarConsts.OP_ADD)), + Production(12, TokenType.ARITH_EXPR, listOf(TokenType.ARITH_EXPR, TokenType.MINUS, TokenType.TERM), + ASTBuilders.createBinaryOp(GrammarConsts.OP_SUB)), + Production(13, TokenType.ARITH_EXPR, listOf(TokenType.TERM), ASTBuilders.Identity), + + Production(14, TokenType.TERM, listOf(TokenType.TERM, TokenType.MULTIPLY, TokenType.FACTOR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_MUL)), + Production(15, TokenType.TERM, listOf(TokenType.TERM, TokenType.DIVIDE, TokenType.FACTOR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_DIV)), + Production(16, TokenType.TERM, listOf(TokenType.TERM, TokenType.MODULO, TokenType.FACTOR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_MOD)), + Production(17, TokenType.TERM, listOf(TokenType.FACTOR), ASTBuilders.Identity), + + // ๊ฑฐ๋“ญ์ œ๊ณฑ + Production(18, TokenType.FACTOR, listOf(TokenType.PRIMARY, TokenType.POWER, TokenType.FACTOR), + ASTBuilders.createBinaryOp(GrammarConsts.OP_POW)), + Production(19, TokenType.FACTOR, listOf(TokenType.PRIMARY), ASTBuilders.Identity), + + // ๋‹จํ•ญ ์—ฐ์‚ฐ / ๊ธฐ๋ณธ ํ•ญ + Production(20, TokenType.PRIMARY, listOf(TokenType.LEFT_PAREN, TokenType.EXPR, TokenType.RIGHT_PAREN), + ASTBuilders.Parenthesized), + Production(21, TokenType.PRIMARY, listOf(TokenType.MINUS, TokenType.PRIMARY), + ASTBuilders.createUnaryOp(GrammarConsts.OP_SUB)), + Production(22, TokenType.PRIMARY, listOf(TokenType.PLUS, TokenType.PRIMARY), + ASTBuilders.createUnaryOp(GrammarConsts.OP_ADD)), + Production(23, TokenType.PRIMARY, listOf(TokenType.NOT, TokenType.PRIMARY), + ASTBuilders.createUnaryOp(GrammarConsts.OP_NOT)), + Production(24, TokenType.PRIMARY, listOf(TokenType.NUMBER), ASTBuilders.Number), + Production(25, TokenType.PRIMARY, listOf(TokenType.VARIABLE), ASTBuilders.Variable), + Production(26, TokenType.PRIMARY, listOf(TokenType.IDENTIFIER), ASTBuilders.Variable), + Production(27, TokenType.PRIMARY, listOf(TokenType.TRUE), ASTBuilders.BooleanTrue), + Production(28, TokenType.PRIMARY, listOf(TokenType.FALSE), ASTBuilders.BooleanFalse), + Production(29, TokenType.PRIMARY, listOf(TokenType.IDENTIFIER, TokenType.LEFT_PAREN, TokenType.ARGS, TokenType.RIGHT_PAREN), + ASTBuilders.FunctionCall), + Production(30, TokenType.PRIMARY, listOf(TokenType.IDENTIFIER, TokenType.LEFT_PAREN, TokenType.RIGHT_PAREN), + ASTBuilders.FunctionCallEmpty), + Production(31, TokenType.PRIMARY, listOf( + TokenType.IF, TokenType.LEFT_PAREN, TokenType.EXPR, TokenType.COMMA, + TokenType.EXPR, TokenType.COMMA, TokenType.EXPR, TokenType.RIGHT_PAREN + ), ASTBuilders.If), + Production(32, TokenType.ARGS, listOf(TokenType.EXPR), ASTBuilders.ArgsSingle), + Production(33, TokenType.ARGS, listOf(TokenType.ARGS, TokenType.COMMA, TokenType.EXPR), + ASTBuilders.ArgsMultiple) + ) + + /** + * ๋ฌธ๋ฒ•์˜ ์‹œ์ž‘ ์‹ฌ๋ณผ์ž…๋‹ˆ๋‹ค. + */ + val startSymbol: TokenType = TokenType.EXPR + + /** + * ํ™•์žฅ๋œ ๋ฌธ๋ฒ•์˜ ์‹œ์ž‘ ์ƒ์„ฑ ๊ทœ์น™์ž…๋‹ˆ๋‹ค. + * LR(1) ํŒŒ์„œ ๊ตฌ์ถ•์„ ์œ„ํ•ด ์ถ”๊ฐ€๋˜๋Š” ๊ทœ์น™์ž…๋‹ˆ๋‹ค. + */ + val augmentedProduction: Production = Production( + -1, + TokenType.START, + listOf(TokenType.EXPR, TokenType.DOLLAR), + ASTBuilders.Start + ) + + /** + * ๋ชจ๋“  ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ์ž…๋‹ˆ๋‹ค. + */ + val terminals: Set = TokenType.getTerminals().toSet() + + /** + * ๋ชจ๋“  ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ์ž…๋‹ˆ๋‹ค. + */ + val nonTerminals: Set = TokenType.getNonTerminals().toSet() + + /** + * ์ฃผ์–ด์ง„ ID์— ํ•ด๋‹นํ•˜๋Š” ์ƒ์„ฑ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getProduction(id: Int): Production { + if (id !in productions.indices) { + throw ParserException.productionIdOutOfRange(id = id, total = productions.size) + } + return productions[id] + } + + /** + * ์ฃผ์–ด์ง„ ๊ทœ์น™ ๋ฌธ์ž์—ด์— ํ•ด๋‹นํ•˜๋Š” ์ƒ์„ฑ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getProductionByRule(rule: String): Production { + return productions.find { it.toString() == rule } + ?: throw ParserException.productionNotFound(rule) + } + + /** + * ํŠน์ • ์ขŒ๋ณ€์„ ๊ฐ€์ง„ ๋ชจ๋“  ์ƒ์„ฑ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getProductionsFor(leftSymbol: TokenType): List = + productions.filter { it.left == leftSymbol } + + /** + * ํŠน์ • ์‹ฌ๋ณผ์„ ํฌํ•จํ•˜๋Š” ๋ชจ๋“  ์ƒ์„ฑ ๊ทœ์น™์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getProductionsContaining(symbol: TokenType): List = + productions.filter { it.containsSymbol(symbol) } + + /** + * ์ขŒ์žฌ๊ท€ ์ƒ์„ฑ ๊ทœ์น™๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getLeftRecursiveProductions(): List = + productions.filter { it.isDirectLeftRecursive() } + + /** + * ์šฐ์žฌ๊ท€ ์ƒ์„ฑ ๊ทœ์น™๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getRightRecursiveProductions(): List = + productions.filter { it.isDirectRightRecursive() } + + /** + * ์—ก์‹ค๋ก  ์ƒ์„ฑ ๊ทœ์น™๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getEpsilonProductions(): List = + productions.filter { it.isEpsilonProduction() } + + /** + * ํŠน์ • ์‹ฌ๋ณผ์ด ํ„ฐ๋ฏธ๋„์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isTerminal(symbol: TokenType): Boolean = symbol in terminals + + /** + * ํŠน์ • ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isNonTerminal(symbol: TokenType): Boolean = symbol in nonTerminals + + /** + * ๋ฌธ๋ฒ•์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getGrammarStatistics(): Map = mapOf( + GrammarConsts.KEY_PRODUCTION_COUNT to productions.size, + GrammarConsts.KEY_TERMINAL_COUNT to terminals.size, + GrammarConsts.KEY_NON_TERMINAL_COUNT to nonTerminals.size, + GrammarConsts.KEY_START_SYMBOL to startSymbol, + GrammarConsts.KEY_LEFT_RECURSIVE_COUNT to getLeftRecursiveProductions().size, + GrammarConsts.KEY_RIGHT_RECURSIVE_COUNT to getRightRecursiveProductions().size, + GrammarConsts.KEY_EPSILON_COUNT to getEpsilonProductions().size, + GrammarConsts.KEY_AVG_PRODUCTION_LEN to productions.map { it.length }.average(), + GrammarConsts.KEY_MAX_PRODUCTION_LEN to (productions.maxOfOrNull { it.length } ?: 0), + GrammarConsts.KEY_MIN_PRODUCTION_LEN to (productions.minOfOrNull { it.length } ?: 0) + ) + + /** + * ๋ฌธ๋ฒ•์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + fun isValid(): Boolean = try { + Production.validateProductions(productions) && + isNonTerminal(startSymbol) && + getProductionsFor(startSymbol).isNotEmpty() && + (terminals intersect nonTerminals).isEmpty() + } catch (e: Exception) { + false + } + + /** + * ๋ฌธ๋ฒ•์„ BNF ํ˜•ํƒœ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + */ + fun toBNFString(): String = buildString { + appendLine("${GrammarConsts.TITLE_GRAMMAR} (${productions.size} productions):") + appendLine("${GrammarConsts.LABEL_START_SYMBOL}$startSymbol") + appendLine("${GrammarConsts.LABEL_TERMINALS}${terminals.joinToString(", ")}") + appendLine("${GrammarConsts.LABEL_NON_TERMINALS}${nonTerminals.joinToString(", ")}") + appendLine() + appendLine(GrammarConsts.LABEL_PRODUCTIONS) + productions.forEach { appendLine(" ${it.toDetailString()}") } + appendLine() + appendLine(GrammarConsts.LABEL_AUGMENTED) + appendLine(" ${augmentedProduction.toDetailString()}") + } + + /** + * ๋ฌธ๋ฒ•์˜ ๊ฐ„๋‹จํ•œ ์š”์•ฝ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getSummary(): String = buildString { + val stats = getGrammarStatistics() + appendLine(GrammarConsts.TITLE_SUMMARY) + appendLine("${GrammarConsts.LABEL_PRODUCTIONS_SUM}${stats[GrammarConsts.KEY_PRODUCTION_COUNT]}") + appendLine("${GrammarConsts.LABEL_TERMINALS_SUM}${stats[GrammarConsts.KEY_TERMINAL_COUNT]}") + appendLine("${GrammarConsts.LABEL_NON_TERMINALS_SUM}${stats[GrammarConsts.KEY_NON_TERMINAL_COUNT]}") + appendLine("${GrammarConsts.LABEL_START_SYMBOL_SUM}${stats[GrammarConsts.KEY_START_SYMBOL]}") + appendLine("${GrammarConsts.LABEL_LEFT_REC_SUM}${stats[GrammarConsts.KEY_LEFT_RECURSIVE_COUNT]}") + appendLine("${GrammarConsts.LABEL_RIGHT_REC_SUM}${stats[GrammarConsts.KEY_RIGHT_RECURSIVE_COUNT]}") + appendLine("${GrammarConsts.LABEL_EPSILON_SUM}${stats[GrammarConsts.KEY_EPSILON_COUNT]}") + appendLine("${GrammarConsts.LABEL_AVG_LEN_SUM}${"%.2f".format(stats[GrammarConsts.KEY_AVG_PRODUCTION_LEN])}") + append("${GrammarConsts.LABEL_VALID_SUM}${isValid()}") + } + + /** + * ํŠน์ • ๋…ผํ„ฐ๋ฏธ๋„์˜ ์ƒ์„ฑ ๊ทœ์น™๋“ค์„ BNF ํ˜•ํƒœ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + */ + fun getProductionsBNF(nonTerminal: TokenType): String { + if (!isNonTerminal(nonTerminal)) throw ParserException.symbolNotNonTerminal(nonTerminal) + + val productionsForSymbol = getProductionsFor(nonTerminal) + if (productionsForSymbol.isEmpty()) { + return "$nonTerminal ${GrammarConsts.ARROW_UNICODE} ${GrammarConsts.NO_PRODUCTIONS}" + } + return buildString { + append("$nonTerminal ${GrammarConsts.ARROW_UNICODE} ") + productionsForSymbol.forEachIndexed { index, production -> + if (index > 0) append(" | ") + append(if (production.right.isEmpty()) GrammarConsts.EPSILON else production.right.joinToString(" ")) + } + } + } + + /** + * ๋ฌธ์ž์—ด ํ‘œํ˜„์œผ๋กœ ์ƒ์„ฑ ๊ทœ์น™์„ ์ฐพ์Šต๋‹ˆ๋‹ค. + */ + fun getProduction(productionString: String): Production { + return productions.find { production -> + val leftStr = production.left.toString() + val rightStr = if (production.right.isEmpty()) GrammarConsts.EPSILON else production.right.joinToString(" ") + val ascii = "$leftStr ${GrammarConsts.ARROW_ASCII} $rightStr" + val unicode = "$leftStr ${GrammarConsts.ARROW_UNICODE} $rightStr" + ascii == productionString || unicode == productionString + } ?: throw ParserException.productionNotFound(productionString) + } + + init { + // ๋ฌธ๋ฒ• ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ + if (!isValid()) throw ParserException.grammarInvalid() + } + + object GrammarConsts { + // ์—ฐ์‚ฐ์ž ๋ฌธ์ž์—ด + const val OP_OR = "||" + const val OP_AND = "&&" + const val OP_EQ = "==" + const val OP_NEQ = "!=" + const val OP_LT = "<" + const val OP_LTE = "<=" + const val OP_GT = ">" + const val OP_GTE = ">=" + const val OP_ADD = "+" + const val OP_SUB = "-" + const val OP_MUL = "*" + const val OP_DIV = "/" + const val OP_MOD = "%" + const val OP_POW = "^" + const val OP_NOT = "!" + + // BNF ์ถœ๋ ฅ์šฉ ์‹ฌ๋ณผ/๋ฌธ์ž์—ด + const val ARROW_ASCII = "->" + const val ARROW_UNICODE = "โ†’" + const val EPSILON = "ฮต" + const val NO_PRODUCTIONS = "(no productions)" + + // getGrammarStatistics ํ‚ค + const val KEY_PRODUCTION_COUNT = "productionCount" + const val KEY_TERMINAL_COUNT = "terminalCount" + const val KEY_NON_TERMINAL_COUNT = "nonTerminalCount" + const val KEY_START_SYMBOL = "startSymbol" + const val KEY_LEFT_RECURSIVE_COUNT = "leftRecursiveCount" + const val KEY_RIGHT_RECURSIVE_COUNT = "rightRecursiveCount" + const val KEY_EPSILON_COUNT = "epsilonCount" + const val KEY_AVG_PRODUCTION_LEN = "avgProductionLength" + const val KEY_MAX_PRODUCTION_LEN = "maxProductionLength" + const val KEY_MIN_PRODUCTION_LEN = "minProductionLength" + + // BNF/์š”์•ฝ ์ถœ๋ ฅ ๋ฌธ๊ตฌ + const val TITLE_GRAMMAR = "Grammar" + const val LABEL_START_SYMBOL = "Start Symbol: " + const val LABEL_TERMINALS = "Terminals: " + const val LABEL_NON_TERMINALS = "Non-terminals: " + const val LABEL_PRODUCTIONS = "Productions:" + const val LABEL_AUGMENTED = "Augmented Production:" + const val TITLE_SUMMARY = "Grammar Summary:" + const val LABEL_PRODUCTIONS_SUM = " Productions: " + const val LABEL_TERMINALS_SUM = " Terminals: " + const val LABEL_NON_TERMINALS_SUM = " Non-terminals: " + const val LABEL_START_SYMBOL_SUM = " Start Symbol: " + const val LABEL_LEFT_REC_SUM = " Left Recursive: " + const val LABEL_RIGHT_REC_SUM = " Right Recursive: " + const val LABEL_EPSILON_SUM = " Epsilon Productions: " + const val LABEL_AVG_LEN_SUM = " Avg Production Length: " + const val LABEL_VALID_SUM = " Valid: " + } +} diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/LRAction.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/LRAction.kt new file mode 100644 index 00000000..1f6be7b9 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/LRAction.kt @@ -0,0 +1,404 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.parser.entities.Production +import hs.kr.entrydsm.domain.parser.exceptions.ParserException +import hs.kr.entrydsm.global.annotation.entities.Entity + +/** + * LR ํŒŒ์„œ๊ฐ€ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋Š” ์•ก์…˜์„ ์ •์˜ํ•˜๋Š” sealed ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * LR(1) ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ํ†ตํ•ด ๊ฒฐ์ •๋˜๋Š” ๋„ค ๊ฐ€์ง€ ์•ก์…˜ ํƒ€์ž…์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค: + * Shift(ํ† ํฐ์„ ์Šคํƒ์— ํ‘ธ์‹œ), Reduce(์ƒ์„ฑ ๊ทœ์น™ ์ ์šฉ), Accept(ํŒŒ์‹ฑ ์™„๋ฃŒ), Error(์˜ค๋ฅ˜ ๋ฐœ์ƒ). + * ๊ฐ ์•ก์…˜์€ ํŒŒ์‹ฑ ์ƒํƒœ์™€ ์ž…๋ ฅ ํ† ํฐ์— ๋”ฐ๋ผ ๊ฒฐ์ •๋˜๋ฉฐ, ํŒŒ์„œ์˜ ๋‹ค์Œ ๋™์ž‘์„ ์ง€์‹œํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +@Entity(context = "parser", aggregateRoot = LRAction::class) +sealed class LRAction { + + /** + * ์•ก์…˜์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์•ก์…˜ ํƒ€์ž… ๋ฌธ์ž์—ด + */ + abstract fun getActionType(): String + + /** + * ์•ก์…˜์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ๋จผ์ € ์ฒ˜๋ฆฌ) + */ + abstract fun getPriority(): Int + + /** + * ์•ก์…˜์ด ์ƒํƒœ๋ฅผ ๋ณ€๊ฒฝํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒํƒœ ๋ณ€๊ฒฝํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + abstract fun changesState(): Boolean + + /** + * ์•ก์…˜์ด ์Šคํƒ์„ ๋ณ€๊ฒฝํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์Šคํƒ ๋ณ€๊ฒฝํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + abstract fun changesStack(): Boolean + + /** + * Shift ์•ก์…˜: ์ž…๋ ฅ ํ† ํฐ์„ ์Šคํƒ์— ํ‘ธ์‹œํ•˜๊ณ  ๋‹ค์Œ ์ƒํƒœ๋กœ ์ „์ดํ•ฉ๋‹ˆ๋‹ค. + * + * ํ˜„์žฌ ์ž…๋ ฅ ํ† ํฐ์„ ํŒŒ์„œ ์Šคํƒ์— ํ‘ธ์‹œํ•˜๊ณ , ์ง€์ •๋œ ๋‹ค์Œ ์ƒํƒœ๋กœ ์ „์ดํ•ฉ๋‹ˆ๋‹ค. + * ์ด๋Š” ์•„์ง ๋” ๋งŽ์€ ์ž…๋ ฅ์ด ํ•„์š”ํ•จ์„ ์˜๋ฏธํ•˜๋ฉฐ, ๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ํŒŒ์‹ฑ ๋™์ž‘์ž…๋‹ˆ๋‹ค. + * + * @property state ์ „์ดํ•  ๋‹ค์Œ ์ƒํƒœ์˜ ID + */ + @Entity(context = "parser", aggregateRoot = LRAction::class) + data class Shift(val state: Int) : LRAction() { + + init { + if (state < 0) { + throw ParserException.stateIdNegative(state) + } + } + + override fun getActionType(): String = LRActionConsts.TYPE_SHIFT + override fun getPriority(): Int = LRActionConsts.PRIORITY_SHIFT + override fun changesState(): Boolean = true + override fun changesStack(): Boolean = true + + /** + * ์ „์ดํ•  ์ƒํƒœ๊ฐ€ ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param maxStateId ์ตœ๋Œ€ ์ƒํƒœ ID + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValidState(maxStateId: Int): Boolean = state <= maxStateId + + override fun toString(): String = "Shift($state)" + } + + /** + * Reduce ์•ก์…˜: ์Šคํƒ์—์„œ ์‹ฌ๋ณผ์„ ํŒํ•˜๊ณ  ์ƒ์„ฑ ๊ทœ์น™์„ ์ ์šฉํ•˜์—ฌ ์ƒˆ๋กœ์šด ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์„ ํ‘ธ์‹œํ•ฉ๋‹ˆ๋‹ค. + * + * ์ง€์ •๋œ ์ƒ์„ฑ ๊ทœ์น™์„ ์ ์šฉํ•˜์—ฌ ์Šคํƒ์—์„œ ์šฐ๋ณ€์˜ ์‹ฌ๋ณผ๋“ค์„ ํŒํ•˜๊ณ , + * AST ๋…ธ๋“œ๋ฅผ ๊ตฌ์ถ•ํ•œ ํ›„ ์ขŒ๋ณ€์˜ ๋…ผํ„ฐ๋ฏธ๋„์„ ์Šคํƒ์— ํ‘ธ์‹œํ•ฉ๋‹ˆ๋‹ค. + * ์ดํ›„ GOTO ํ…Œ์ด๋ธ”์„ ์ฐธ์กฐํ•˜์—ฌ ๋‹ค์Œ ์ƒํƒœ๋กœ ์ „์ดํ•ฉ๋‹ˆ๋‹ค. + * + * @property production ์ ์šฉํ•  ์ƒ์„ฑ ๊ทœ์น™ + */ + @Entity(context = "parser", aggregateRoot = LRAction::class) + data class Reduce(val production: Production) : LRAction() { + + override fun getActionType(): String = LRActionConsts.TYPE_REDUCE + override fun getPriority(): Int = LRActionConsts.PRIORITY_REDUCE + override fun changesState(): Boolean = true + override fun changesStack(): Boolean = true + + /** + * ํŒํ•  ์‹ฌ๋ณผ์˜ ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ฑ ๊ทœ์น™ ์šฐ๋ณ€์˜ ๊ธธ์ด + */ + fun getPopCount(): Int = production.length + + /** + * ์ƒ์„ฑ ๊ทœ์น™์˜ ์ขŒ๋ณ€ ์‹ฌ๋ณผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ขŒ๋ณ€ ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ + */ + fun getLeftSymbol() = production.left + + /** + * ์ƒ์„ฑ ๊ทœ์น™์˜ ์šฐ๋ณ€ ์‹ฌ๋ณผ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ๋ณ€ ์‹ฌ๋ณผ ๋ฆฌ์ŠคํŠธ + */ + fun getRightSymbols() = production.right + + /** + * ์ƒ์„ฑ ๊ทœ์น™ ID๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ฑ ๊ทœ์น™ ID + */ + override fun getProductionId(): Int = production.id + + /** + * ์—ก์‹ค๋ก  ์ƒ์„ฑ ๊ทœ์น™์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ก์‹ค๋ก  ์ƒ์„ฑ์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isEpsilonReduction(): Boolean = production.isEpsilonProduction() + + /** + * AST ๋…ธ๋“œ๋ฅผ ๊ตฌ์ถ•ํ•ฉ๋‹ˆ๋‹ค. + * + * @param children ์ž์‹ ์‹ฌ๋ณผ๋“ค + * @return ๊ตฌ์ถ•๋œ AST ๋…ธ๋“œ ๋˜๋Š” ์‹ฌ๋ณผ + */ + fun buildAST(children: List): Any = production.buildAST(children) + + override fun toString(): String = "Reduce(${production.id}: $production)" + } + + /** + * Accept ์•ก์…˜: ํŒŒ์‹ฑ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์™„๋ฃŒ๋˜์—ˆ์Œ์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + * + * ์ž…๋ ฅ์ด ๋ฌธ๋ฒ•์— ๋”ฐ๋ผ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํŒŒ์‹ฑ๋˜์—ˆ์œผ๋ฉฐ, ํŒŒ์‹ฑ ๊ณผ์ •์ด ์„ฑ๊ณต์ ์œผ๋กœ + * ์™„๋ฃŒ๋˜์—ˆ์Œ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์ด ์•ก์…˜์€ ์‹œ์ž‘ ์‹ฌ๋ณผ๊ณผ EOF๋ฅผ ๋งŒ๋‚ฌ์„ ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. + */ + @Entity(context = "parser", aggregateRoot = LRAction::class) + object Accept : LRAction() { + + override fun getActionType(): String = LRActionConsts.TYPE_ACCEPT + override fun getPriority(): Int = LRActionConsts.PRIORITY_ACCEPT + override fun changesState(): Boolean = false + override fun changesStack(): Boolean = false + + /** + * ํŒŒ์‹ฑ์ด ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•ญ์ƒ true (Accept๋Š” ์„ฑ๊ณต์„ ์˜๋ฏธ) + */ + fun isSuccess(): Boolean = true + + override fun toString(): String = "Accept" + } + + /** + * Error ์•ก์…˜: ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Œ์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + * + * ํ˜„์žฌ ์ƒํƒœ์™€ ์ž…๋ ฅ ํ† ํฐ์˜ ์กฐํ•ฉ์ด ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์— ์ •์˜๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ๋ฐœ์ƒํ•˜๋ฉฐ, + * ๊ตฌ๋ฌธ ์˜ค๋ฅ˜๋‚˜ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ํ† ํฐ์„ ์˜๋ฏธํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ฉ”์ปค๋‹ˆ์ฆ˜์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. + * + * @property errorCode ์˜ค๋ฅ˜ ์ฝ”๋“œ (์„ ํƒ์‚ฌํ•ญ) + * @property errorMessage ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ (์„ ํƒ์‚ฌํ•ญ) + */ + @Entity(context = "parser", aggregateRoot = LRAction::class) + data class Error( + val errorCode: String? = null, + val errorMessage: String? = null + ) : LRAction() { + + override fun getActionType(): String = LRActionConsts.TYPE_ERROR + override fun getPriority(): Int = LRActionConsts.PRIORITY_ERROR + override fun changesState(): Boolean = false + override fun changesStack(): Boolean = false + + /** + * ์˜ค๋ฅ˜ ์ •๋ณด๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ค๋ฅ˜ ์ •๋ณด๊ฐ€ ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasErrorInfo(): Boolean = errorCode != null || errorMessage != null + + /** + * ์™„์ „ํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ค๋ฅ˜ ์ฝ”๋“œ์™€ ๋ฉ”์‹œ์ง€๊ฐ€ ๊ฒฐํ•ฉ๋œ ๋ฌธ์ž์—ด + */ + fun getFullErrorMessage(): String = when { + errorCode != null && errorMessage != null -> "[${errorCode}] ${errorMessage}" + errorCode != null -> "[${errorCode}] ${LRActionConsts.MSG_PARSE_ERROR_DEFAULT}" + errorMessage != null -> errorMessage + else -> LRActionConsts.MSG_PARSE_ERROR_DEFAULT + } + + override fun toString(): String = if (hasErrorInfo()) { + "Error(${getFullErrorMessage()})" + } else { + "Error" + } + } + + /** + * ์•ก์…˜์ด ์ข…๋ฃŒ ์•ก์…˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return Accept ๋˜๋Š” Error์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isTerminalAction(): Boolean = this is Accept || this is Error + + /** + * ์•ก์…˜์ด ์„ฑ๊ณต ์•ก์…˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return Accept์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSuccessAction(): Boolean = this is Accept + + /** + * ์•ก์…˜์ด ์˜ค๋ฅ˜ ์•ก์…˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return Error์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isErrorAction(): Boolean = this is Error + + /** + * ์•ก์…˜์ด ์ƒํƒœ ์ „์ด ์•ก์…˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return Shift ๋˜๋Š” Reduce์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isStateTransitionAction(): Boolean = this is Shift || this is Reduce + + /** + * ์•ก์…˜์ด Shift ์•ก์…˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return Shift์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isShift(): Boolean = this is Shift + + /** + * ์•ก์…˜์ด Reduce ์•ก์…˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return Reduce์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isReduce(): Boolean = this is Reduce + + /** + * ์•ก์…˜์ด Accept ์•ก์…˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return Accept์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAccept(): Boolean = this is Accept + + /** + * Reduce ์•ก์…˜์˜ ๊ฒฝ์šฐ ์ƒ์‚ฐ ๊ทœ์น™ ID๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์‚ฐ ๊ทœ์น™ ID + * @throws IllegalStateException Reduce ์•ก์…˜์ด ์•„๋‹Œ ๊ฒฝ์šฐ + */ + open fun getProductionId(): Int { + throw ParserException.notReduceAction(this.getActionType()) + } + + /** + * ์•ก์…˜์˜ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์•ก์…˜ ์ •๋ณด ๋งต + */ + fun getActionInfo(): Map = when (this) { + is Shift -> mapOf( + LRActionConsts.KEY_TYPE to getActionType(), + LRActionConsts.KEY_STATE to state, + LRActionConsts.KEY_PRIORITY to getPriority(), + LRActionConsts.KEY_CHANGES_STATE to changesState(), + LRActionConsts.KEY_CHANGES_STACK to changesStack() + ) + is Reduce -> mapOf( + LRActionConsts.KEY_TYPE to getActionType(), + LRActionConsts.KEY_PRODUCTION_ID to production.id, + LRActionConsts.KEY_PRODUCTION to production.toString(), + LRActionConsts.KEY_POP_COUNT to getPopCount(), + LRActionConsts.KEY_LEFT_SYMBOL to getLeftSymbol(), + LRActionConsts.KEY_PRIORITY to getPriority(), + LRActionConsts.KEY_CHANGES_STATE to changesState(), + LRActionConsts.KEY_CHANGES_STACK to changesStack() + ) + is Accept -> mapOf( + LRActionConsts.KEY_TYPE to getActionType(), + LRActionConsts.KEY_PRIORITY to getPriority(), + LRActionConsts.KEY_CHANGES_STATE to changesState(), + LRActionConsts.KEY_CHANGES_STACK to changesStack(), + LRActionConsts.KEY_IS_SUCCESS to true + ) + is Error -> mapOf( + LRActionConsts.KEY_TYPE to getActionType(), + LRActionConsts.KEY_ERROR_CODE to (errorCode ?: LRActionConsts.UNKNOWN), + LRActionConsts.KEY_ERROR_MESSAGE to (errorMessage ?: LRActionConsts.MSG_PARSE_ERROR_UNKNOWN), + LRActionConsts.KEY_FULL_MESSAGE to getFullErrorMessage(), + LRActionConsts.KEY_PRIORITY to getPriority(), + LRActionConsts.KEY_CHANGES_STATE to changesState(), + LRActionConsts.KEY_CHANGES_STACK to changesStack() + ) + } + + companion object { + /** + * ์•ก์…˜๋“ค์„ ์šฐ์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ์ •๋ ฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param actions ์ •๋ ฌํ•  ์•ก์…˜ ๋ฆฌ์ŠคํŠธ + * @return ์šฐ์„ ์ˆœ์œ„ ์ˆœ์œผ๋กœ ์ •๋ ฌ๋œ ์•ก์…˜ ๋ฆฌ์ŠคํŠธ + */ + fun sortByPriority(actions: List): List = + actions.sortedByDescending { it.getPriority() } + + /** + * ์•ก์…˜ ๋ฆฌ์ŠคํŠธ์—์„œ ์ตœ๊ณ  ์šฐ์„ ์ˆœ์œ„ ์•ก์…˜์„ ์„ ํƒํ•ฉ๋‹ˆ๋‹ค. + * + * @param actions ์„ ํƒํ•  ์•ก์…˜ ๋ฆฌ์ŠคํŠธ + * @return ์ตœ๊ณ  ์šฐ์„ ์ˆœ์œ„ ์•ก์…˜ (์—†์œผ๋ฉด null) + */ + fun selectHighestPriority(actions: List): LRAction? = + actions.maxByOrNull { it.getPriority() } + + /** + * ์•ก์…˜ ์ถฉ๋Œ์„ ๊ฐ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param actions ํ™•์ธํ•  ์•ก์…˜ ๋ฆฌ์ŠคํŠธ + * @return ์ถฉ๋Œ์ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasConflict(actions: List): Boolean = + actions.size > 1 && actions.any { !it.isErrorAction() } + + /** + * Shift/Reduce ์ถฉ๋Œ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param actions ํ™•์ธํ•  ์•ก์…˜ ๋ฆฌ์ŠคํŠธ + * @return Shift/Reduce ์ถฉ๋Œ์ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + + fun hasShiftReduceConflict(actions: List): Boolean = + actions.any { it is Shift } && actions.any { it is Reduce } + + /** + * Reduce/Reduce ์ถฉ๋Œ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param actions ํ™•์ธํ•  ์•ก์…˜ ๋ฆฌ์ŠคํŠธ + * @return Reduce/Reduce ์ถฉ๋Œ์ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasReduceReduceConflict(actions: List): Boolean { + val reduceActions = actions.filterIsInstance() + return reduceActions.size > 1 + } + } + + /** + * LRAction์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ƒ์ˆ˜ ๋ชจ์Œ + */ + object LRActionConsts { + // Action type strings + const val TYPE_SHIFT = "SHIFT" + const val TYPE_REDUCE = "REDUCE" + const val TYPE_ACCEPT = "ACCEPT" + const val TYPE_ERROR = "ERROR" + + // Priorities (higher = earlier) + const val PRIORITY_ERROR = 0 + const val PRIORITY_REDUCE = 1 + const val PRIORITY_SHIFT = 2 + const val PRIORITY_ACCEPT = 4 + + // Generic messages + const val MSG_PARSE_ERROR_DEFAULT = "ํŒŒ์‹ฑ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" + const val MSG_PARSE_ERROR_UNKNOWN = "Unknown error" + + // Map keys for getActionInfo() + const val KEY_TYPE = "type" + const val KEY_STATE = "state" + const val KEY_PRIORITY = "priority" + const val KEY_CHANGES_STATE = "changesState" + const val KEY_CHANGES_STACK = "changesStack" + const val KEY_PRODUCTION_ID = "productionId" + const val KEY_PRODUCTION = "production" + const val KEY_POP_COUNT = "popCount" + const val KEY_LEFT_SYMBOL = "leftSymbol" + const val KEY_IS_SUCCESS = "isSuccess" + const val KEY_ERROR_CODE = "errorCode" + const val KEY_ERROR_MESSAGE = "errorMessage" + const val KEY_FULL_MESSAGE = "fullMessage" + + // Other literals + const val UNKNOWN = "UNKNOWN" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/OperatorPrecedence.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/OperatorPrecedence.kt new file mode 100644 index 00000000..ce2381e6 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/OperatorPrecedence.kt @@ -0,0 +1,182 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.exceptions.ParserException + +/** + * ์—ฐ์‚ฐ์ž์˜ ์šฐ์„ ์ˆœ์œ„์™€ ๊ฒฐํ•ฉ์„ฑ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ํŒŒ์„œ์˜ Shift/Reduce ์ถฉ๋Œ ํ•ด๊ฒฐ์— ์‚ฌ์šฉ๋˜๋ฉฐ, ์ˆ˜ํ•™ ํ‘œํ˜„์‹์˜ + * ์ •ํ™•ํ•œ ํŒŒ์‹ฑ์„ ์œ„ํ•ด ํ•„์ˆ˜์ ์ธ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @property precedence ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ๋จผ์ € ๊ณ„์‚ฐ) + * @property associativity ๊ฒฐํ•ฉ์„ฑ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.16 + */ +data class OperatorPrecedence( + val precedence: Int, + val associativity: Associativity.AssociativityType +) { + + init { + if (precedence < 0) { + throw ParserException.precedenceNegative(precedence) + } + } + + /** + * ํ˜„์žฌ ์—ฐ์‚ฐ์ž๊ฐ€ ๋‹ค๋ฅธ ์—ฐ์‚ฐ์ž๋ณด๋‹ค ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๋‹ค๋ฅธ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ + * @return ํ˜„์žฌ ์—ฐ์‚ฐ์ž์˜ ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๋” ๋†’์œผ๋ฉด true + */ + fun hasHigherPrecedenceThan(other: OperatorPrecedence): Boolean = + precedence > other.precedence + + /** + * ํ˜„์žฌ ์—ฐ์‚ฐ์ž๊ฐ€ ๋‹ค๋ฅธ ์—ฐ์‚ฐ์ž์™€ ๊ฐ™์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๋‹ค๋ฅธ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ + * @return ์šฐ์„ ์ˆœ์œ„๊ฐ€ ๊ฐ™์œผ๋ฉด true + */ + fun hasSamePrecedenceAs(other: OperatorPrecedence): Boolean = + precedence == other.precedence + + /** + * ์ขŒ๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ขŒ๊ฒฐํ•ฉ์ด๋ฉด true + */ + fun isLeftAssociative(): Boolean = associativity.isLeft() + + /** + * ์šฐ๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ๊ฒฐํ•ฉ์ด๋ฉด true + */ + fun isRightAssociative(): Boolean = associativity.isRight() + + /** + * ๋น„๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋น„๊ฒฐํ•ฉ์ด๋ฉด true + */ + fun isNonAssociative(): Boolean = associativity.isNone() + + /** + * ์ฒด์ธ๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฒด์ธ๊ฒฐํ•ฉ์ด๋ฉด true + */ + fun isChainAssociative(): Boolean = associativity.isChain() + + companion object { + /** ๊ฐ€์žฅ ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„ */ + val LOWEST = OperatorPrecedence(0, Associativity.AssociativityType.LEFT) + + /** ๊ฐ€์žฅ ๋†’์€ ์šฐ์„ ์ˆœ์œ„ */ + val HIGHEST = OperatorPrecedence(Int.MAX_VALUE, Associativity.AssociativityType.LEFT) + + /** ๋…ผ๋ฆฌ OR ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ */ + val LOGICAL_OR = OperatorPrecedence(1, Associativity.AssociativityType.LEFT) + + /** ๋…ผ๋ฆฌ AND ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ */ + val LOGICAL_AND = OperatorPrecedence(2, Associativity.AssociativityType.LEFT) + + /** ๋™๋“ฑ ๋น„๊ต ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ */ + val EQUALITY = OperatorPrecedence(3, Associativity.AssociativityType.LEFT) + + /** ๊ด€๊ณ„ ๋น„๊ต ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ */ + val RELATIONAL = OperatorPrecedence(4, Associativity.AssociativityType.LEFT) + + /** ๋ง์…ˆ/๋บ„์…ˆ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ */ + val ADDITIVE = OperatorPrecedence(5, Associativity.AssociativityType.LEFT) + + /** ๊ณฑ์…ˆ/๋‚˜๋ˆ—์…ˆ/๋‚˜๋จธ์ง€ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ */ + val MULTIPLICATIVE = OperatorPrecedence(6, Associativity.AssociativityType.LEFT) + + /** ๋‹จํ•ญ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ */ + val UNARY = OperatorPrecedence(7, Associativity.AssociativityType.RIGHT) + + /** ๊ฑฐ๋“ญ์ œ๊ณฑ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ */ + val POWER = OperatorPrecedence(8, Associativity.AssociativityType.RIGHT) + + /** + * ์—ฐ์‚ฐ์ž๋ณ„ ์šฐ์„ ์ˆœ์œ„ ํ…Œ์ด๋ธ” + */ + private val precedenceTable = mapOf( + // ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž + TokenType.OR to LOGICAL_OR, + TokenType.AND to LOGICAL_AND, + TokenType.NOT to UNARY, + + // ๋น„๊ต ์—ฐ์‚ฐ์ž + TokenType.EQUAL to EQUALITY, + TokenType.NOT_EQUAL to EQUALITY, + TokenType.LESS to RELATIONAL, + TokenType.LESS_EQUAL to RELATIONAL, + TokenType.GREATER to RELATIONAL, + TokenType.GREATER_EQUAL to RELATIONAL, + + // ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž + TokenType.PLUS to ADDITIVE, + TokenType.MINUS to ADDITIVE, + TokenType.MULTIPLY to MULTIPLICATIVE, + TokenType.DIVIDE to MULTIPLICATIVE, + TokenType.MODULO to MULTIPLICATIVE, + TokenType.POWER to POWER + ) + + /** + * ์ง€์ •๋œ ํ† ํฐ์˜ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ์กฐํšŒํ•  ํ† ํฐ ํƒ€์ž… + * @return ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๋˜๋Š” null (์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ) + */ + fun getPrecedence(token: TokenType): OperatorPrecedence? = + precedenceTable[token] + + /** + * ์ง€์ •๋œ ํ† ํฐ์ด ์—ฐ์‚ฐ์ž์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param token ํ™•์ธํ•  ํ† ํฐ ํƒ€์ž… + * @return ์—ฐ์‚ฐ์ž์ด๋ฉด true + */ + fun isOperator(token: TokenType): Boolean = + token in precedenceTable + + /** + * ๋ชจ๋“  ์—ฐ์‚ฐ์ž ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฐ์‚ฐ์ž ํ† ํฐ ์ง‘ํ•ฉ + */ + fun getAllOperators(): Set = + precedenceTable.keys + + /** + * ์šฐ์„ ์ˆœ์œ„๋ณ„๋กœ ๊ทธ๋ฃนํ™”๋œ ์—ฐ์‚ฐ์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์„ ์ˆœ์œ„ -> ์—ฐ์‚ฐ์ž ๋ชฉ๋ก ๋งต + */ + fun getOperatorsByPrecedence(): Map> = + precedenceTable.entries + .groupBy { it.value.precedence } + .mapValues { (_, entries) -> entries.map { it.key } } + + /** + * ์‚ฌ์šฉ์ž ์ •์˜ ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param precedence ์šฐ์„ ์ˆœ์œ„ + * @param associativity ๊ฒฐํ•ฉ์„ฑ + * @return ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ ๊ฐ์ฒด + */ + fun custom(precedence: Int, associativity: Associativity.AssociativityType): OperatorPrecedence = + OperatorPrecedence(precedence, associativity) + } + + override fun toString(): String = "Precedence($precedence, $associativity)" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParseSymbol.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParseSymbol.kt new file mode 100644 index 00000000..6e67b888 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParseSymbol.kt @@ -0,0 +1,147 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.parser.exceptions.ParserException + +/** + * ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ์Šคํƒ์— ์ €์žฅ๋˜๋Š” ์‹ฌ๋ณผ์˜ ํƒ€์ž… ์•ˆ์ „ ํ‘œํ˜„์„ ์ œ๊ณตํ•˜๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ํ† ํฐ๊ณผ AST ๋…ธ๋“œ๋ฅผ ๊ตฌ๋ถ„ํ•˜์—ฌ ์ปดํŒŒ์ผ ํƒ€์ž„ ํƒ€์ž… ๊ฒ€์ฆ์„ ์ œ๊ณตํ•˜๋ฉฐ, + * ํŒŒ์‹ฑ ์Šคํƒ์˜ ์•ˆ์ „์„ฑ๊ณผ ์ •ํ™•์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * POC ์ฝ”๋“œ์˜ ParseSymbol์„ DDD ๊ตฌ์กฐ๋กœ ์žฌ๊ตฌ์„ฑํ•˜์—ฌ ๊ตฌํ˜„ํ•˜์˜€์Šต๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.16 + */ +sealed class ParseSymbol { + + /** + * ์‹ฌ๋ณผ ํƒ€์ž…, ํฌ๊ธฐ ๋“ฑ ๊ณ ์ • ๊ฐ’ ์ƒ์ˆ˜๋ฅผ ๋ชจ์•„๋‘” ๊ฐ์ฒด + */ + object Constants { + const val TOKEN_TYPE = "TOKEN" + const val AST_TYPE = "AST" + const val ARGUMENTS_TYPE = "ARGUMENTS" + + const val TOKEN_SIZE_OFFSET = 16 + const val AST_SIZE_OFFSET = 32 + const val ARGUMENTS_SIZE_OFFSET = 16 + } + + abstract fun getSymbolType(): String + abstract fun getSymbolSize(): Int + abstract fun isTerminal(): Boolean + abstract fun getStringRepresentation(): String + + /** + * ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ (ํ† ํฐ)์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด + */ + data class TokenSymbol(val token: Token) : ParseSymbol() { + init { + require(token.value.isNotEmpty()) { + throw ParserException.tokenValueEmpty(token) + } + } + + override fun getSymbolType(): String = Constants.TOKEN_TYPE + override fun getSymbolSize(): Int = token.value.length + Constants.TOKEN_SIZE_OFFSET + override fun isTerminal(): Boolean = true + override fun getStringRepresentation(): String = token.toString() + + fun getTokenType() = token.type + fun getTokenValue(): String = token.value + fun getTokenPosition(): Int = token.position.index + fun isTokenType(expectedType: Any): Boolean = token.type == expectedType + + companion object { + fun of(token: Token): TokenSymbol = TokenSymbol(token) + } + } + + /** + * ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ (AST ๋…ธ๋“œ)์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด + */ + data class ASTSymbol(val node: ASTNode) : ParseSymbol() { + override fun getSymbolType(): String = Constants.AST_TYPE + override fun getSymbolSize(): Int = estimateASTSize(node) + override fun isTerminal(): Boolean = false + override fun getStringRepresentation(): String = node.toString() + + fun getNodeType(): String = node.javaClass.simpleName + fun getVariables(): Set = node.getVariables() + inline fun isNodeType(): Boolean = node is T + inline fun asNodeType(): T? = node as? T + fun getDepth(): Int = 1 // ์ถ”ํ›„ ์‹ค์ œ ๊ตฌํ˜„ ์‹œ ์ˆ˜์ • ๊ฐ€๋Šฅ + + companion object { + fun of(node: ASTNode): ASTSymbol = ASTSymbol(node) + private fun estimateASTSize(node: ASTNode): Int = + node.toString().length + Constants.AST_SIZE_OFFSET + } + } + + /** + * ํ•จ์ˆ˜ ์ธ์ˆ˜ ๋ชฉ๋ก์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด + */ + data class ArgumentsSymbol(val args: List) : ParseSymbol() { + init { + require(args.isNotEmpty()) { + throw ParserException.argumentsEmpty() + } + } + + override fun getSymbolType(): String = Constants.ARGUMENTS_TYPE + override fun getSymbolSize(): Int = + args.sumOf { it.toString().length } + Constants.ARGUMENTS_SIZE_OFFSET + override fun isTerminal(): Boolean = false + override fun getStringRepresentation(): String = + "Args[${args.joinToString(", ") { it.toString() }}]" + + fun getArgumentCount(): Int = args.size + fun getArgument(index: Int): ASTNode { + if (index !in args.indices) { + throw ParserException.argumentIndexOutOfRange(index, args.size) + } + return args[index] + } + + fun getFirstArgument(): ASTNode = args.first() + fun getLastArgument(): ASTNode = args.last() + fun getAllVariables(): Set = + args.flatMap { it.getVariables() }.toSet() + fun addArgument(newArg: ASTNode): ArgumentsSymbol = ArgumentsSymbol(args + newArg) + fun toList(): List = args.toList() + + companion object { + fun single(arg: ASTNode): ArgumentsSymbol = ArgumentsSymbol(listOf(arg)) + fun of(args: List): ArgumentsSymbol = ArgumentsSymbol(args) + fun empty(): ArgumentsSymbol = ArgumentsSymbol(emptyList()) + } + } + + companion object { + fun fromToken(token: Token): ParseSymbol = TokenSymbol.of(token) + fun fromAST(node: ASTNode): ParseSymbol = ASTSymbol.of(node) + fun fromArguments(args: List): ParseSymbol = ArgumentsSymbol.of(args) + + fun from(obj: Any): ParseSymbol = when (obj) { + is Token -> fromToken(obj) + is ASTNode -> fromAST(obj) + is List<*> -> { + @Suppress("UNCHECKED_CAST") + fromArguments(obj as List) + } + else -> throw ParserException.unsupportedObjectType(obj.javaClass.simpleName) + } + + fun calculateTotalSize(symbols: List): Int = + symbols.sumOf { it.getSymbolSize() } + + fun countTerminals(symbols: List): Int = + symbols.count { it.isTerminal() } + + fun countNonTerminals(symbols: List): Int = + symbols.count { !it.isTerminal() } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParserState.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParserState.kt new file mode 100644 index 00000000..ab169646 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParserState.kt @@ -0,0 +1,174 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.lexer.entities.Token + +/** + * ํŒŒ์„œ์˜ ์‹คํ–‰ ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ๊ด€๋ฆฌ๋˜๋Š” ๋ชจ๋“  ์ƒํƒœ ์ •๋ณด๋ฅผ ์บก์Аํ™”ํ•˜์—ฌ + * ์ƒํƒœ ๊ด€๋ฆฌ์˜ ์ผ๊ด€์„ฑ๊ณผ ๋ถˆ๋ณ€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * ์—ฌ๋Ÿฌ ํ•„๋“œ์— ๋Œ€ํ•œ ์ง์ ‘ ์กฐ์ž‘์„ ๋ฐฉ์ง€ํ•˜๊ณ  ์‘์ง‘์„ฑ์„ ๋†’์ž…๋‹ˆ๋‹ค. + * + * @property stateStack LR ํŒŒ์„œ์˜ ์ƒํƒœ ์Šคํƒ + * @property astStack AST ๋…ธ๋“œ ์Šคํƒ + * @property currentPosition ํ˜„์žฌ ํ† ํฐ ์œ„์น˜ + * @property inputTokens ์ž…๋ ฅ ํ† ํฐ ๋ชฉ๋ก + * @property parsingSteps ํŒŒ์‹ฑ ๋‹จ๊ณ„ ์ˆ˜ + * @property shiftOperations ์‹œํ”„ํŠธ ์—ฐ์‚ฐ ์ˆ˜ + * @property reduceOperations ๋ฆฌ๋“€์Šค ์—ฐ์‚ฐ ์ˆ˜ + * @property errorRecoveryAttempts ์˜ค๋ฅ˜ ๋ณต๊ตฌ ์‹œ๋„ ํšŸ์ˆ˜ + * @property parsingTrace ํŒŒ์‹ฑ ์ถ”์  ์ •๋ณด + * + * @author kangeunchan + * @since 2025.08.11 + */ +data class ParserState( + val stateStack: MutableList = mutableListOf(), + val astStack: MutableList = mutableListOf(), + var currentPosition: Int = 0, + val inputTokens: MutableList = mutableListOf(), + var parsingSteps: Int = 0, + var shiftOperations: Int = 0, + var reduceOperations: Int = 0, + var errorRecoveryAttempts: Int = 0, + val parsingTrace: MutableList = mutableListOf() +) { + + /** + * ํŒŒ์„œ ์ƒํƒœ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun initialize(tokens: List) { + stateStack.clear() + astStack.clear() + inputTokens.clear() + parsingTrace.clear() + + stateStack.add(0) // ์ดˆ๊ธฐ ์ƒํƒœ + inputTokens.addAll(tokens) + currentPosition = 0 + parsingSteps = 0 + shiftOperations = 0 + reduceOperations = 0 + errorRecoveryAttempts = 0 + } + + /** + * ํ˜„์žฌ ํ† ํฐ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun currentToken(): Token? { + return if (currentPosition < inputTokens.size) { + inputTokens[currentPosition] + } else null + } + + /** + * ํ˜„์žฌ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun currentState(): Int { + return stateStack.lastOrNull() ?: 0 + } + + /** + * ์‹œํ”„ํŠธ ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + fun performShift(newState: Int, token: Token) { + stateStack.add(newState) + // ์ž„์‹œ๋กœ null ์ถ”๊ฐ€ (๋‚˜์ค‘์— ์‹ค์ œ AST ๋…ธ๋“œ ์ƒ์„ฑ์œผ๋กœ ๊ต์ฒด) + astStack.add(null) + currentPosition++ + shiftOperations++ + parsingSteps++ + + if (parsingTrace.isNotEmpty() || isTraceEnabled()) { + parsingTrace.add(ParsingTraceEntry.shift(newState, token, currentState(), parsingSteps)) + } + } + + /** + * ๋ฆฌ๋“€์Šค ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + */ + fun performReduce(production: hs.kr.entrydsm.domain.parser.entities.Production): ASTNode? { + val popCount = production.right.size + + // ์Šคํƒ์—์„œ ์‹ฌ๋ณผ๋“ค์„ ํŒ + val children = mutableListOf() + repeat(popCount) { + if (stateStack.isNotEmpty()) stateStack.removeLastOrNull() + children.add(0, astStack.removeLastOrNull()) + } + + // AST ๋…ธ๋“œ ์ƒ์„ฑ + val astNode = production.astBuilder.build(children.filterNotNull()) + astStack.add(astNode as? ASTNode) + + reduceOperations++ + parsingSteps++ + + if (parsingTrace.isNotEmpty() || isTraceEnabled()) { + parsingTrace.add(ParsingTraceEntry.reduce(production, currentState(), parsingSteps)) + } + + return astNode as? ASTNode + } + + /** + * ์—๋Ÿฌ ๋ณต๊ตฌ๋ฅผ ์‹œ๋„ํ•ฉ๋‹ˆ๋‹ค. + */ + fun attemptErrorRecovery(): Boolean { + errorRecoveryAttempts++ + + // ๊ฐ„๋‹จํ•œ ์—๋Ÿฌ ๋ณต๊ตฌ: ํ˜„์žฌ ํ† ํฐ ์Šคํ‚ต + if (currentPosition < inputTokens.size - 1) { + currentPosition++ + return true + } + + return false + } + + /** + * ํŒŒ์‹ฑ์ด ์™„๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isComplete(): Boolean { + return currentPosition >= inputTokens.size && astStack.size == 1 + } + + /** + * ์Šคํƒ ํฌ๊ธฐ ์ œํ•œ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isStackSizeValid(maxSize: Int): Boolean { + return stateStack.size <= maxSize && astStack.size <= maxSize + } + + /** + * ํŒŒ์‹ฑ ๋‹จ๊ณ„ ์ œํ•œ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + fun isStepLimitValid(maxSteps: Int): Boolean { + return parsingSteps <= maxSteps + } + + /** + * ์ถ”์ ์ด ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + private fun isTraceEnabled(): Boolean { + // ์‹ค์ œ ๊ตฌํ˜„์—์„œ๋Š” ์„ค์ •์—์„œ ๊ฐ€์ ธ์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค + return false + } + + /** + * ํŒŒ์‹ฑ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getStatistics(): Map { + return mapOf( + "parsingSteps" to parsingSteps, + "shiftOperations" to shiftOperations, + "reduceOperations" to reduceOperations, + "errorRecoveryAttempts" to errorRecoveryAttempts, + "currentPosition" to currentPosition, + "stackDepth" to stateStack.size, + "astStackSize" to astStack.size, + "remainingTokens" to (inputTokens.size - currentPosition).coerceAtLeast(0) + ) + } +} diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParsingResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParsingResult.kt new file mode 100644 index 00000000..c683ae8f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParsingResult.kt @@ -0,0 +1,345 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.ast.entities.ASTNode +import hs.kr.entrydsm.domain.ast.entities.NumberNode +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.parser.exceptions.ParserException + +/** + * ๊ตฌ๋ฌธ ๋ถ„์„(Parsing) ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * Parser์˜ ํ† ํฐ ๋ถ„์„ ๊ฒฐ๊ณผ๋กœ, ์ƒ์„ฑ๋œ AST์™€ ๋ถ„์„ ๊ณผ์ •์—์„œ ๋ฐœ์ƒํ•œ + * ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. ์„ฑ๊ณต/์‹คํŒจ ์—ฌ๋ถ€์™€ ๊ด€๋ จ ์ •๋ณด๋ฅผ ํ•จ๊ป˜ ์ œ๊ณตํ•˜์—ฌ + * Evaluator์—์„œ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” ์™„์ „ํ•œ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @property ast ์ƒ์„ฑ๋œ ์ถ”์ƒ ๊ตฌ๋ฌธ ํŠธ๋ฆฌ + * @property isSuccess ๋ถ„์„ ์„ฑ๊ณต ์—ฌ๋ถ€ + * @property error ๋ถ„์„ ์‹คํŒจ ์‹œ์˜ ์˜ค๋ฅ˜ ์ •๋ณด + * @property duration ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + * @property tokenCount ์ฒ˜๋ฆฌ๋œ ํ† ํฐ ๊ฐœ์ˆ˜ + * @property nodeCount ์ƒ์„ฑ๋œ AST ๋…ธ๋“œ ๊ฐœ์ˆ˜ + * @property maxDepth AST์˜ ์ตœ๋Œ€ ๊นŠ์ด + * @property warnings ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +data class ParsingResult( + val ast: ASTNode?, + val isSuccess: Boolean = true, + val error: ParserException? = null, + val duration: Long = 0L, + val tokenCount: Int = 0, + val nodeCount: Int = 0, + val maxDepth: Int = 0, + val warnings: List = emptyList(), + val metadata: Map = emptyMap() +) { + + init { + if (!isSuccess && error == null) { + throw ParserException.failedResultMissingError() + } + if (duration < 0L) { + throw ParserException.durationNegative(duration) + } + if (tokenCount < 0) { + throw ParserException.tokenCountNegative(tokenCount) + } + if (nodeCount < 0) { + throw ParserException.nodeCountNegative(nodeCount) + } + if (maxDepth < 0) { + throw ParserException.maxDepthNegative(maxDepth) + } + if (isSuccess && ast == null) { + throw ParserException.successResultMissingAst() + } + } + + companion object { + /** + * ์„ฑ๊ณต์ ์ธ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param ast ์ƒ์„ฑ๋œ AST + * @param duration ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„ + * @param tokenCount ์ฒ˜๋ฆฌ๋œ ํ† ํฐ ๊ฐœ์ˆ˜ + * @param nodeCount ์ƒ์„ฑ๋œ ๋…ธ๋“œ ๊ฐœ์ˆ˜ + * @param maxDepth AST ์ตœ๋Œ€ ๊นŠ์ด + * @param warnings ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก + * @param metadata ์ถ”๊ฐ€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ + * @return ์„ฑ๊ณต ParsingResult + */ + fun success( + ast: ASTNode, + duration: Long = 0L, + tokenCount: Int = 0, + nodeCount: Int = 0, + maxDepth: Int = 0, + warnings: List = emptyList(), + metadata: Map = emptyMap() + ): ParsingResult = ParsingResult( + ast = ast, + isSuccess = true, + error = null, + duration = duration, + tokenCount = tokenCount, + nodeCount = nodeCount, + maxDepth = maxDepth, + warnings = warnings, + metadata = metadata + ) + + /** + * ์‹คํŒจํ•œ ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param error ๋ถ„์„ ์˜ค๋ฅ˜ ์ •๋ณด + * @param partialAST ๋ถ€๋ถ„์ ์œผ๋กœ ์ƒ์„ฑ๋œ AST + * @param duration ๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„ + * @param tokenCount ์ฒ˜๋ฆฌ๋œ ํ† ํฐ ๊ฐœ์ˆ˜ + * @param nodeCount ์ƒ์„ฑ๋œ ๋…ธ๋“œ ๊ฐœ์ˆ˜ + * @param maxDepth AST ์ตœ๋Œ€ ๊นŠ์ด + * @param warnings ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ๋ชฉ๋ก + * @param metadata ์ถ”๊ฐ€ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ + * @return ์‹คํŒจ ParsingResult + */ + fun failure( + error: ParserException, + partialAST: ASTNode? = null, + duration: Long = 0L, + tokenCount: Int = 0, + nodeCount: Int = 0, + maxDepth: Int = 0, + warnings: List = emptyList(), + metadata: Map = emptyMap() + ): ParsingResult = ParsingResult( + ast = partialAST, + isSuccess = false, + error = error, + duration = duration, + tokenCount = tokenCount, + nodeCount = nodeCount, + maxDepth = maxDepth, + warnings = warnings, + metadata = metadata + ) + + /** + * ๋นˆ ์„ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param tokenCount ์ฒ˜๋ฆฌ๋œ ํ† ํฐ ๊ฐœ์ˆ˜ + * @return ๋นˆ ์„ฑ๊ณต ParsingResult + */ + fun empty(tokenCount: Int = 0): ParsingResult { + // ๋นˆ AST ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๋ฅผ ์œ„ํ•œ ๋”๋ฏธ ๋…ธ๋“œ + val emptyNode = NumberNode(0.0) + return success( + ast = emptyNode, + tokenCount = tokenCount + ) + } + } + + /** + * ๋ถ„์„ ์‹คํŒจ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹คํŒจํ–ˆ์œผ๋ฉด true + */ + fun isFailure(): Boolean = !isSuccess + + /** + * AST๊ฐ€ ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return AST๊ฐ€ ์žˆ์œผ๋ฉด true + */ + fun hasAST(): Boolean = ast != null + + /** + * ๊ฒฝ๊ณ ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฒฝ๊ณ ๊ฐ€ ์žˆ์œผ๋ฉด true + */ + fun hasWarnings(): Boolean = warnings.isNotEmpty() + + /** + * ์˜ค๋ฅ˜๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ค๋ฅ˜๊ฐ€ ์žˆ์œผ๋ฉด true + */ + fun hasError(): Boolean = error != null + + /** + * ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด true + */ + fun hasMetadata(): Boolean = metadata.isNotEmpty() + + /** + * ํŠน์ • ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ‚ค + * @return ํ•ด๋‹น ํ‚ค์˜ ๊ฐ’ ๋˜๋Š” null + */ + fun getMetadata(key: String): Any? = metadata[key] + + /** + * AST์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return AST ํด๋ž˜์Šค๋ช… ๋˜๋Š” "None" + */ + fun getASTType(): String = ast?.javaClass?.simpleName ?: ParsingResultConsts.STR_NONE + + /** + * ํŒŒ์‹ฑ ํšจ์œจ์„ฑ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค (๋…ธ๋“œ ์ˆ˜ / ํ† ํฐ ์ˆ˜). + * + * @return ํšจ์œจ์„ฑ ๋น„์œจ (0.0 ~ 1.0+) + */ + fun getParsingEfficiency(): Double = + if (tokenCount > 0) nodeCount.toDouble() / tokenCount else 0.0 + + /** + * ์ดˆ๋‹น ์ฒ˜๋ฆฌ๋œ ํ† ํฐ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ† ํฐ ์ฒ˜๋ฆฌ ์†๋„ (tokens/second) + */ + fun getTokensPerSecond(): Double = + if (duration > 0) + (tokenCount * ParsingResultConsts.MS_TO_SEC_MULTIPLIER) / duration + else 0.0 + + /** + * AST์˜ ํ‰๊ท  ๋ถ„๊ธฐ ๊ณ„์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ‰๊ท  ๋ถ„๊ธฐ ๊ณ„์ˆ˜ + */ + fun getAverageBranchingFactor(): Double = + if (maxDepth > 0) nodeCount.toDouble() / maxDepth else 0.0 + + /** + * ๋ถ„์„ ๊ฒฐ๊ณผ์˜ ํ’ˆ์งˆ ์ ์ˆ˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ’ˆ์งˆ ์ ์ˆ˜ (0.0 ~ 100.0) + */ + fun getQualityScore(): Double { + var score = if (isSuccess) ParsingResultConsts.QUALITY_BASE_SUCCESS else 0.0 + + // ๊ฒฝ๊ณ  ์ ์ˆ˜ ์ฐจ๊ฐ + score -= warnings.size * ParsingResultConsts.QUALITY_WARNING_PENALTY + + // ํšจ์œจ์„ฑ ๋ณด๋„ˆ์Šค + score += getParsingEfficiency() * ParsingResultConsts.QUALITY_EFFICIENCY_BONUS + + // ์„ฑ๋Šฅ(TPS) ๋ณด๋„ˆ์Šค + if (duration > 0 && tokenCount > 0) { + val tps = getTokensPerSecond() + val perfBonus = (tps / ParsingResultConsts.QUALITY_TPS_NORM) * ParsingResultConsts.QUALITY_TPS_MULTIPLIER + score += minOf(perfBonus, ParsingResultConsts.QUALITY_TPS_CAP) + } + + return maxOf( + ParsingResultConsts.QUALITY_MIN, + minOf(ParsingResultConsts.QUALITY_MAX, score) + ) + } + + /** + * ๋ถ„์„ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ + fun getStatistics(): Map = mapOf( + ParsingResultConsts.KEY_SUCCESS to isSuccess, + ParsingResultConsts.KEY_TOKEN_COUNT to tokenCount, + ParsingResultConsts.KEY_NODE_COUNT to nodeCount, + ParsingResultConsts.KEY_MAX_DEPTH to maxDepth, + ParsingResultConsts.KEY_DURATION to duration, + ParsingResultConsts.KEY_WARNING_COUNT to warnings.size, + ParsingResultConsts.KEY_HAS_ERROR to hasError(), + ParsingResultConsts.KEY_AST_TYPE to getASTType(), + ParsingResultConsts.KEY_PARSING_EFFICIENCY to getParsingEfficiency(), + ParsingResultConsts.KEY_TOKENS_PER_SECOND to getTokensPerSecond(), + ParsingResultConsts.KEY_AVG_BRANCH_FACTOR to getAverageBranchingFactor(), + ParsingResultConsts.KEY_QUALITY_SCORE to getQualityScore(), + ParsingResultConsts.KEY_ERROR_MESSAGE to (error?.message ?: ParsingResultConsts.STR_NONE) + ) + + /** + * AST๋ฅผ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return AST ๋ฌธ์ž์—ด ํ‘œํ˜„ + */ + fun astToString(): String = ast?.toString() ?: ParsingResultConsts.STR_NULL + + /** + * ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€๋“ค์„ ๋ฌธ์ž์—ด๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ๋ฌธ์ž์—ด + */ + fun warningsToString(): String = warnings.joinToString("; ") + + /** + * ๋ถ„์„ ๊ฒฐ๊ณผ์˜ ์š”์•ฝ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์š”์•ฝ ์ •๋ณด ๋ฌธ์ž์—ด + */ + fun getSummary(): String = buildString { + append("ParsingResult(") + append("success=$isSuccess, ") + append("tokens=$tokenCount, ") + append("nodes=$nodeCount, ") + append("duration=${duration}ms") + if (warnings.isNotEmpty()) { + append(", warnings=${warnings.size}") + } + if (error != null) { + append(", error=${error.message}") + } + append(")") + } + + /** + * ๋ถ„์„ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ธ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ์ •๋ณด ๋ฌธ์ž์—ด + */ + override fun toString(): String = getSummary() + + /** + * ParsingResult์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ƒ์ˆ˜ ๋ชจ์Œ + */ + object ParsingResultConsts { + // Map keys (getStatistics) + const val KEY_SUCCESS = "success" + const val KEY_TOKEN_COUNT = "tokenCount" + const val KEY_NODE_COUNT = "nodeCount" + const val KEY_MAX_DEPTH = "maxDepth" + const val KEY_DURATION = "duration" + const val KEY_WARNING_COUNT = "warningCount" + const val KEY_HAS_ERROR = "hasError" + const val KEY_AST_TYPE = "astType" + const val KEY_PARSING_EFFICIENCY = "parsingEfficiency" + const val KEY_TOKENS_PER_SECOND = "tokensPerSecond" + const val KEY_AVG_BRANCH_FACTOR = "averageBranchingFactor" + const val KEY_QUALITY_SCORE = "qualityScore" + const val KEY_ERROR_MESSAGE = "errorMessage" + + // String literals + const val STR_NONE = "None" + const val STR_NULL = "null" + + // Performance/score constants + const val MS_TO_SEC_MULTIPLIER = 1000.0 + + const val QUALITY_BASE_SUCCESS = 50.0 + const val QUALITY_WARNING_PENALTY = 5.0 + const val QUALITY_EFFICIENCY_BONUS = 20.0 + const val QUALITY_TPS_NORM = 1000.0 + const val QUALITY_TPS_MULTIPLIER = 10.0 + const val QUALITY_TPS_CAP = 30.0 + const val QUALITY_MIN = 0.0 + const val QUALITY_MAX = 100.0 + } +} diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParsingTable.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParsingTable.kt new file mode 100644 index 00000000..dc6d11f8 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParsingTable.kt @@ -0,0 +1,167 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.lexer.entities.TokenType +import hs.kr.entrydsm.domain.parser.entities.ParsingState +import hs.kr.entrydsm.domain.parser.exceptions.ParserException + +/** + * LR ํŒŒ์‹ฑ ํ…Œ์ด๋ธ”์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * LR ํŒŒ์„œ์—์„œ ์‚ฌ์šฉํ•˜๋Š” Action ํ…Œ์ด๋ธ”๊ณผ Goto ํ…Œ์ด๋ธ”์„ ํฌํ•จํ•˜๋ฉฐ, + * ํŒŒ์‹ฑ ๊ณผ์ •์—์„œ ํ˜„์žฌ ์ƒํƒœ์™€ ์ž…๋ ฅ ์‹ฌ๋ณผ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๋‹ค์Œ ๋™์ž‘์„ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + * DDD Value Object ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๋ถˆ๋ณ€์„ฑ๊ณผ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @property states ๋ชจ๋“  ํŒŒ์‹ฑ ์ƒํƒœ๋“ค + * @property actionTable Action ํ…Œ์ด๋ธ” (state, terminal) -> action + * @property gotoTable Goto ํ…Œ์ด๋ธ” (state, non-terminal) -> state + * @property startState ์‹œ์ž‘ ์ƒํƒœ ID + * @property acceptStates ์ˆ˜๋ฝ ์ƒํƒœ ID๋“ค + * @property terminals ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * @property nonTerminals ๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ ์ง‘ํ•ฉ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +data class ParsingTable( + val states: Map, + val actionTable: Map, LRAction>, + val gotoTable: Map, Int>, + val startState: Int = 0, + val acceptStates: Set = emptySet(), + val terminals: Set = emptySet(), + val nonTerminals: Set = emptySet(), + val metadata: Map = emptyMap() +) { + + companion object { + fun empty(): ParsingTable { + val emptyState = ParsingState.createEmpty(0) + return ParsingTable( + states = mapOf(0 to emptyState), + actionTable = emptyMap(), + gotoTable = emptyMap(), + startState = 0, + acceptStates = emptySet(), + terminals = emptySet(), + nonTerminals = emptySet() + ) + } + + fun build( + states: List, + startStateId: Int = 0, + terminals: Set, + nonTerminals: Set + ): ParsingTable { + val stateMap = states.associateBy { it.id } + val actionTable = mutableMapOf, LRAction>() + val gotoTable = mutableMapOf, Int>() + val acceptStates = mutableSetOf() + + states.forEach { state -> + state.actions.forEach { (terminal, action) -> + actionTable[state.id to terminal] = action + } + state.gotos.forEach { (nonTerminal, targetState) -> + gotoTable[state.id to nonTerminal] = targetState + } + if (state.isAccepting) { + acceptStates.add(state.id) + } + } + + return ParsingTable( + states = stateMap, + actionTable = actionTable, + gotoTable = gotoTable, + startState = startStateId, + acceptStates = acceptStates.toSet(), + terminals = terminals, + nonTerminals = nonTerminals + ) + } + } + + fun getAction(stateId: Int, terminal: TokenType): LRAction? { + if (stateId !in states) { + throw ParserException.invalidStateId(stateId) + } + if (!terminal.isTerminal) { + throw ParserException.terminalSymbolRequired(terminal) + } + return actionTable[stateId to terminal] + } + + fun getGoto(stateId: Int, nonTerminal: TokenType): Int? { + if (stateId !in states) { + throw ParserException.invalidStateId(stateId) + } + if (!nonTerminal.isNonTerminal()) { + throw ParserException.nonTerminalSymbolRequired(nonTerminal) + } + return gotoTable[stateId to nonTerminal] + } + + fun getState(stateId: Int): ParsingState { + return states[stateId] ?: throw ParserException.stateNotFound(stateId) + } + + fun getStartState(): ParsingState = getState(startState) + + fun getAcceptStates(): List { + return acceptStates.map { getState(it) } + } + + fun getConflicts(): Map> { + val conflicts = mutableMapOf>() + states.values.forEach { state -> + val stateConflicts = state.getConflicts() + stateConflicts.forEach { (type, details) -> + conflicts.getOrPut(type) { mutableListOf() } + .addAll(details.map { "${ParsingTableConsts.STATE_PREFIX} ${state.id}: $it" }) + } + } + return conflicts + } + + fun isLR1Valid(): Boolean { + if (getConflicts().isNotEmpty()) return false + if (states.values.any { !it.isConsistent() }) return false + if (startState !in states) return false + if (acceptStates.isEmpty()) return false + return true + } + + fun getActionsForState(stateId: Int): Map { + if (stateId !in states) { + throw ParserException.invalidStateId(stateId) + } + return actionTable.filter { it.key.first == stateId } + .mapKeys { it.key.second } + } + + fun getGotosForState(stateId: Int): Map { + if (stateId !in states) { + throw ParserException.invalidStateId(stateId) + } + return gotoTable.filter { it.key.first == stateId } + .mapKeys { it.key.second } + } + + override fun toString(): String = + ParsingTableConsts.SUMMARY_TEMPLATE.format( + states.size, + actionTable.size, + gotoTable.size + ) + + /** + * ParsingTable์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ƒ์ˆ˜ ๋ชจ์Œ + */ + object ParsingTableConsts { + const val STATE_PREFIX = "State" + const val SUMMARY_TEMPLATE = "ParsingTable(states=%d, actions=%d, gotos=%d)" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParsingTraceEntry.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParsingTraceEntry.kt new file mode 100644 index 00000000..3c2d9b24 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/parser/values/ParsingTraceEntry.kt @@ -0,0 +1,71 @@ +package hs.kr.entrydsm.domain.parser.values + +import hs.kr.entrydsm.domain.lexer.entities.Token +import hs.kr.entrydsm.domain.parser.entities.Production + +/** + * ํŒŒ์‹ฑ ์ถ”์  ํ•ญ๋ชฉ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ํŒŒ์‹ฑ ๊ณผ์ •์˜ ๊ฐ ๋‹จ๊ณ„๋ฅผ ์ถ”์ ํ•˜๊ธฐ ์œ„ํ•œ ์ •๋ณด๋ฅผ ๋‹ด๊ณ  ์žˆ์œผ๋ฉฐ, + * ๋””๋ฒ„๊น…๊ณผ ํŒŒ์‹ฑ ๋ถ„์„์— ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @property step ํŒŒ์‹ฑ ๋‹จ๊ณ„ ๋ฒˆํ˜ธ + * @property action ์ˆ˜ํ–‰๋œ ์•ก์…˜ (SHIFT, REDUCE, ERROR ๋“ฑ) + * @property state ํŒŒ์‹ฑ ์ƒํƒœ ID + * @property token ์ฒ˜๋ฆฌ๋œ ํ† ํฐ (null์ผ ์ˆ˜ ์žˆ์Œ) + * @property production ์ ์šฉ๋œ ์ƒ์‚ฐ ๊ทœ์น™ (Reduce ์•ก์…˜์ธ ๊ฒฝ์šฐ) + * @property stackSnapshot ์Šคํƒ ์ƒํƒœ ์Šค๋ƒ…์ƒท + * + * @author kangeunchan + * @since 2025.08.11 + */ +data class ParsingTraceEntry( + val step: Int, + val action: String, + val state: Int, + val token: Token?, + val production: Production?, + val stackSnapshot: List +) { + companion object { + fun shift(newState: Int, token: Token, currentState: Int, parsingSteps: Int): ParsingTraceEntry { + return ParsingTraceEntry( + step = parsingSteps, + action = ParsingTraceEntryConsts.ACTION_SHIFT, + state = newState, + token = token, + production = null, + stackSnapshot = listOf(currentState, newState) + ) + } + + fun reduce(production: Production, currentState: Int, parsingSteps: Int): ParsingTraceEntry { + return ParsingTraceEntry( + step = parsingSteps, + action = ParsingTraceEntryConsts.ACTION_REDUCE, + state = currentState, + token = null, + production = production, + stackSnapshot = listOf(currentState) + ) + } + } + + override fun toString(): String { + val tokenInfo = if (token != null) ", token: ${token.type}" else "" + val productionInfo = if (production != null) ", production: ${production.id}" else "" + return ParsingTraceEntryConsts.TO_STRING_TEMPLATE.format( + step, action, state, tokenInfo, productionInfo, stackSnapshot + ) + } + + + /** + * ParsingTraceEntry์—์„œ ์‚ฌ์šฉํ•˜๋Š” ์ƒ์ˆ˜ ๋ชจ์Œ + */ + object ParsingTraceEntryConsts { + const val ACTION_SHIFT = "SHIFT" + const val ACTION_REDUCE = "REDUCE" + const val TO_STRING_TEMPLATE = "Step %d: %s at state %d%s%s, stack: %s" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/Score.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/Score.kt deleted file mode 100644 index 4656542d..00000000 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/Score.kt +++ /dev/null @@ -1,18 +0,0 @@ -package hs.kr.entrydsm.domain.score.model - -import hs.kr.entrydsm.domain.score.model.types.Achievement -import hs.kr.entrydsm.domain.score.model.types.Field -import hs.kr.entrydsm.domain.score.model.types.Subject -import java.util.UUID - -data class Score( - val id: UUID = UUID.randomUUID(), - val userId: UUID, - - val field: Field, - - val subject: Subject?, - val achievement: Achievement?, - - val score: Short -) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/types/Achievement.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/types/Achievement.kt deleted file mode 100644 index 3d0fb222..00000000 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/types/Achievement.kt +++ /dev/null @@ -1,10 +0,0 @@ -package hs.kr.entrydsm.domain.score.model.types - -enum class Achievement{ - A, - B, - C, - D, - E, - NONE -} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/types/Field.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/types/Field.kt deleted file mode 100644 index b7fcfbd0..00000000 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/types/Field.kt +++ /dev/null @@ -1,15 +0,0 @@ -package hs.kr.entrydsm.domain.score.model.types - -enum class Field { - COURSE_SCORE, - ATTENDANCE_SCORE, - VOLUNTEER_SCORE, - BONUS_SCORE, - VOCATIONAL_BASIC_SCORE, - INTERVIEW_SCORE, - COMPUTING_THINKING_SCORE, - FIRST_STAGE_TOTAL, - SECOND_STAGE_TOTAL, - FINAL_TOTAL, - NONE -} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/types/Subject.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/types/Subject.kt deleted file mode 100644 index 4d5adf0c..00000000 --- a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/score/model/types/Subject.kt +++ /dev/null @@ -1,11 +0,0 @@ -package hs.kr.entrydsm.domain.score.model.types - -enum class Subject{ - KOREAN, - ENGLISH, - MATH, - SOCIAL, - SCIENCE, - HISTORY, - TECHNOLOGY -} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/util/TypeUtils.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/util/TypeUtils.kt new file mode 100644 index 00000000..0a118b4e --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/domain/util/TypeUtils.kt @@ -0,0 +1,120 @@ +package hs.kr.entrydsm.domain.util + +import kotlin.reflect.KClass + +/** + * ํƒ€์ž… ๊ด€๋ จ ์œ ํ‹ธ๋ฆฌํ‹ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ํƒ€์ž… ๊ฒ€์ฆ, ํ˜ธํ™˜์„ฑ ํ™•์ธ, ๋ณ€ํ™˜ ๋“ฑ์˜ ๊ณตํ†ต ๊ธฐ๋Šฅ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ์—์„œ ์ค‘๋ณต๋˜๋˜ ํƒ€์ž… ๊ด€๋ จ ๋กœ์ง์„ ์ค‘์•™ํ™”ํ•˜์—ฌ + * ์ฝ”๋“œ ์ค‘๋ณต์„ ์ œ๊ฑฐํ•˜๊ณ  ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.06 + */ +object TypeUtils { + + /** + * ์ˆซ์ž ํƒ€์ž…๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜์ž…๋‹ˆ๋‹ค. + * Kotlin์˜ ๋ชจ๋“  ์ˆซ์ž ํƒ€์ž…์„ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + */ + val NUMERIC_TYPES = setOf( + Byte::class, + Short::class, + Int::class, + Long::class, + Float::class, + Double::class + ) + + /** + * ์ฃผ์–ด์ง„ ํƒ€์ž…์ด ์ˆซ์ž ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ™•์ธํ•  ํƒ€์ž… + * @return ์ˆซ์ž ํƒ€์ž…์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isNumericType(type: KClass<*>): Boolean { + return NUMERIC_TYPES.contains(type) + } + + /** + * ์ฃผ์–ด์ง„ ๊ฐ์ฒด๊ฐ€ ์ˆซ์ž ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ํ™•์ธํ•  ๊ฐ์ฒด + * @return ์ˆซ์ž ํƒ€์ž…์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isNumericValue(value: Any?): Boolean { + return when (value) { + is Byte, is Short, is Int, is Long, is Float, is Double -> true + else -> false + } + } + + /** + * ๋‘ ํƒ€์ž…์ด ๋ชจ๋‘ ์ˆซ์ž ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param type1 ์ฒซ ๋ฒˆ์งธ ํƒ€์ž… + * @param type2 ๋‘ ๋ฒˆ์งธ ํƒ€์ž… + * @return ๋‘˜ ๋‹ค ์ˆซ์ž ํƒ€์ž…์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun areBothNumericTypes(type1: KClass<*>, type2: KClass<*>): Boolean { + return isNumericType(type1) && isNumericType(type2) + } + + /** + * ์ฃผ์–ด์ง„ ํƒ€์ž…์ด ๋ถˆ๋ฆฐ ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ™•์ธํ•  ํƒ€์ž… + * @return ๋ถˆ๋ฆฐ ํƒ€์ž…์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isBooleanType(type: KClass<*>): Boolean { + return type == Boolean::class + } + + /** + * ์ฃผ์–ด์ง„ ํƒ€์ž…์ด ๋ฌธ์ž์—ด ํƒ€์ž…์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param type ํ™•์ธํ•  ํƒ€์ž… + * @return ๋ฌธ์ž์—ด ํƒ€์ž…์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isStringType(type: KClass<*>): Boolean { + return type == String::class + } + + /** + * ์ˆซ์ž ํƒ€์ž…์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * ํƒ€์ž… ๋ณ€ํ™˜ ์‹œ ๋” ๋†’์€ ์šฐ์„ ์ˆœ์œ„ ํƒ€์ž…์œผ๋กœ ๋ณ€ํ™˜๋ฉ๋‹ˆ๋‹ค. + * + * @param type ์ˆซ์ž ํƒ€์ž… + * @return ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ์šฐ์„ ) + */ + fun getNumericTypePriority(type: KClass<*>): Int { + return when (type) { + Byte::class -> 1 + Short::class -> 2 + Int::class -> 3 + Long::class -> 4 + Float::class -> 5 + Double::class -> 6 + else -> 0 // ์ˆซ์ž ํƒ€์ž…์ด ์•„๋‹Œ ๊ฒฝ์šฐ + } + } + + /** + * ๋‘ ์ˆซ์ž ํƒ€์ž… ์ค‘ ๋” ๋†’์€ ์šฐ์„ ์ˆœ์œ„ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param type1 ์ฒซ ๋ฒˆ์งธ ์ˆซ์ž ํƒ€์ž… + * @param type2 ๋‘ ๋ฒˆ์งธ ์ˆซ์ž ํƒ€์ž… + * @return ๋” ๋†’์€ ์šฐ์„ ์ˆœ์œ„์˜ ํƒ€์ž… + */ + fun getHigherPriorityType(type1: KClass<*>, type2: KClass<*>): KClass<*> { + if (!isNumericType(type1) || !isNumericType(type2)) { + return Any::class + } + + val priority1 = getNumericTypePriority(type1) + val priority2 = getNumericTypePriority(type2) + + return if (priority1 >= priority2) type1 else type2 + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/DomainEvent.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/DomainEvent.kt new file mode 100644 index 00000000..ce8fbea5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/DomainEvent.kt @@ -0,0 +1,15 @@ +package hs.kr.entrydsm.global.annotation + +/** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ์ž„์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. + * + * DDD Event Sourcing ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ๋„๋ฉ”์ธ์—์„œ ๋ฐœ์ƒํ•˜๋Š” ์ค‘์š”ํ•œ ์‚ฌ๊ฑด๋“ค์„ + * ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. ์ด ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ํด๋ž˜์Šค๋Š” ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋กœ ์ทจ๊ธ‰๋ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.21 + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class DomainEvent \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/aggregates/Aggregate.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/aggregates/Aggregate.kt new file mode 100644 index 00000000..8a6b8103 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/aggregates/Aggregate.kt @@ -0,0 +1,22 @@ +package hs.kr.entrydsm.global.annotation.aggregates + +/** + * Aggregate Root ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์–ด๋…ธํ…Œ์ด์…˜ ์ž…๋‹ˆ๋‹ค. + * + * Aggregate๋Š” ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ์˜ ๋‹จ์œ„๋กœ ์ทจ๊ธ‰๋˜๋Š” ์—ฐ๊ด€๋œ ๊ฐ์ฒด๋“ค์˜ ์ง‘ํ•ฉ ์˜๋ฏธํ•˜๋ฉฐ, + *์ด ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ํด๋ž˜์Šค๋Š” ํ•ด๋‹น Aggregate์˜ ๋ฃจํŠธ ์—”ํ‹ฐํ‹ฐ์ž„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + * + * @param context Aggregate๊ฐ€ ์†ํ•œ Bounded Context๋ฅผ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * + * @author kangeunchan + * @since 2025.07.08 + * */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Aggregate( + val context: String, +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/aggregates/AggregateContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/aggregates/AggregateContract.kt new file mode 100644 index 00000000..1f932667 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/aggregates/AggregateContract.kt @@ -0,0 +1,27 @@ +package hs.kr.entrydsm.global.annotation.aggregates + +/** + * DDD์˜ Aggregate Root ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค๊ฐ€ ๋”ฐ๋ผ์•ผ ํ•˜๋Š” ๊ณ„์•ฝ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * Aggregate Root๋Š” ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ์˜ ์ง„์ž…์  ์—ญํ• ์„ ํ•˜๋ฉฐ, + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์˜ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•˜๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.15 + */ +interface AggregateContract { + + /** + * ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ + */ + fun getContext(): String + + /** + * ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ์˜ ๊ณ ์œ  ์‹๋ณ„์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ์˜ ๊ณ ์œ  ์‹๋ณ„์ž + */ + fun getId(): Any +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/aggregates/provider/AggregateProvider.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/aggregates/provider/AggregateProvider.kt new file mode 100644 index 00000000..a54591f0 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/aggregates/provider/AggregateProvider.kt @@ -0,0 +1,164 @@ +package hs.kr.entrydsm.global.annotation.aggregates.provider + +import hs.kr.entrydsm.global.annotation.aggregates.Aggregate +import hs.kr.entrydsm.global.annotation.aggregates.AggregateContract + +/** + * DDD์˜ Aggregate Root ํŒจํ„ด ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” Provider ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ์˜ ๋“ฑ๋ก, ์กฐํšŒ, ๊ฒ€์ฆ์„ ํ†ตํ•ด ๋„๋ฉ”์ธ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ๋“ค์„ + * ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์ปจํ…์ŠคํŠธ๋ณ„ ๋ถ„๋ฅ˜๋ฅผ ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * ์ฃผ์š” ๊ธฐ๋Šฅ: + * - ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค ๋“ฑ๋ก ๋ฐ ๊ด€๋ฆฌ + * - ์ปจํ…์ŠคํŠธ๋ณ„ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ์กฐํšŒ + * - ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ์œ ํšจ์„ฑ ๊ฒ€์ฆ (์–ด๋…ธํ…Œ์ด์…˜, ์ธํ„ฐํŽ˜์ด์Šค) + * - ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ-์ปจํ…์ŠคํŠธ ๊ด€๊ณ„ ๊ด€๋ฆฌ + * - ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ + * + * @author kangeunchan + * @since 2025.07.15 + */ +object AggregateProvider { + + private val aggregateRegistry = mutableMapOf>>() + private val aggregateCache = mutableMapOf, String>() + + /** + * ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋กœ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๋“ฑ๋กํ•  ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค ํƒ€์ž… + */ + inline fun registerAggregate() { + registerAggregate(T::class.java) + } + + /** + * ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateClass ๋“ฑ๋กํ•  ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค + * @param T ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค ํƒ€์ž… + * @throws IllegalArgumentException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun registerAggregate(aggregateClass: Class) { + validateAggregate(aggregateClass) + val context = getContextFromAnnotation(aggregateClass) + aggregateRegistry.getOrPut(context) { mutableSetOf() }.add(aggregateClass) + aggregateCache[aggregateClass] = context + } + + /** + * ํŠน์ • ์ปจํ…์ŠคํŠธ์— ์†ํ•œ ๋ชจ๋“  ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param context ๋Œ€์ƒ ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ + * @return ํ•ด๋‹น ์ปจํ…์ŠคํŠธ์— ์†ํ•œ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค ์ง‘ํ•ฉ + */ + fun getAggregatesByContext(context: String): Set> { + return aggregateRegistry[context] ?: emptySet() + } + + /** + * ๋“ฑ๋ก๋œ ๋ชจ๋“  ์ปจํ…์ŠคํŠธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋“ฑ๋ก๋œ ๋ชจ๋“  ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ ์ง‘ํ•ฉ + */ + fun getAllContexts(): Set { + return aggregateRegistry.keys.toSet() + } + + /** + * ๋“ฑ๋ก๋œ ๋ชจ๋“  ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋“ฑ๋ก๋œ ๋ชจ๋“  ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค ์ง‘ํ•ฉ + */ + fun getAllAggregates(): Set> { + return aggregateCache.keys.toSet() + } + + /** + * ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateClass ๋Œ€์ƒ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค + * @return ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ + * @throws IllegalArgumentException @Aggregate ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun getAggregateContext(aggregateClass: Class<*>): String { + return aggregateCache[aggregateClass] + ?: getContextFromAnnotation(aggregateClass) + } + + /** + * ์ฃผ์–ด์ง„ ํด๋ž˜์Šค๊ฐ€ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param clazz ํ™•์ธํ•  ํด๋ž˜์Šค + * @return ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ์ด๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + */ + fun isAggregate(clazz: Class<*>): Boolean { + return aggregateCache.containsKey(clazz) || hasAggregateAnnotation(clazz) + } + + /** + * ๋ชจ๋“  ๋“ฑ๋ก๋œ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun clearAll() { + aggregateRegistry.clear() + aggregateCache.clear() + } + + /** + * ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateClass ๊ฒ€์ฆํ•  ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค + * @throws IllegalArgumentException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateAggregate(aggregateClass: Class<*>) { + validateAggregateAnnotation(aggregateClass) + validateAggregateContract(aggregateClass) + } + + /** + * ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค์— @Aggregate ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateClass ๊ฒ€์ฆํ•  ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค + * @throws IllegalArgumentException @Aggregate ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun validateAggregateAnnotation(aggregateClass: Class<*>) { + aggregateClass.getAnnotation(Aggregate::class.java) + ?: throw IllegalArgumentException("ํด๋ž˜์Šค ${aggregateClass.simpleName}์— @Aggregate ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค.") + } + + /** + * ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค๊ฐ€ AggregateContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateClass ๊ฒ€์ฆํ•  ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค + * @throws IllegalArgumentException AggregateContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateAggregateContract(aggregateClass: Class<*>) { + if (!AggregateContract::class.java.isAssignableFrom(aggregateClass)) { + throw IllegalArgumentException("ํด๋ž˜์Šค ${aggregateClass.simpleName}๋Š” AggregateContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + } + } + + /** + * @Aggregate ์–ด๋…ธํ…Œ์ด์…˜์—์„œ ์ปจํ…์ŠคํŠธ ์ด๋ฆ„์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateClass ๋Œ€์ƒ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ํด๋ž˜์Šค + * @return ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ + * @throws IllegalArgumentException @Aggregate ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun getContextFromAnnotation(aggregateClass: Class<*>): String { + val annotation = aggregateClass.getAnnotation(Aggregate::class.java) + ?: throw IllegalArgumentException("ํด๋ž˜์Šค ${aggregateClass.simpleName}์— @Aggregate ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค.") + return annotation.context + } + + /** + * ํด๋ž˜์Šค์— @Aggregate ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param clazz ํ™•์ธํ•  ํด๋ž˜์Šค + * @return @Aggregate ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ์œผ๋ฉด true, ์—†์œผ๋ฉด false + */ + fun hasAggregateAnnotation(clazz: Class<*>): Boolean { + return clazz.getAnnotation(Aggregate::class.java) != null + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/entities/Entity.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/entities/Entity.kt new file mode 100644 index 00000000..d3a20811 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/entities/Entity.kt @@ -0,0 +1,25 @@ +package hs.kr.entrydsm.global.annotation.entities + +import kotlin.reflect.KClass + +/** + * DDD(Domain-Driven Design)์˜ Entity๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. + * + * Entity๋Š” ๊ณ ์œ ํ•œ ์‹๋ณ„์ž๋ฅผ ๊ฐ€์ง€๋ฉฐ ์ƒ๋ช…์ฃผ๊ธฐ ๋™์•ˆ ์ƒํƒœ๊ฐ€ ๋ณ€ํ•  ์ˆ˜ ์žˆ๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * Aggregate Root๊ฐ€ ์•„๋‹Œ Entity๋Š” ๋ฐ˜๋“œ์‹œ Aggregate Root๋ฅผ ํ†ตํ•ด์„œ๋งŒ ์ ‘๊ทผ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateRoot ์ด Entity๊ฐ€ ์†ํ•œ Aggregate Root ํด๋ž˜์Šค + * @param context Entity๊ฐ€ ์†ํ•œ Bounded Context๋ฅผ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.08 + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Entity( + val aggregateRoot: KClass<*>, + val context: String, +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/entities/EntityContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/entities/EntityContract.kt new file mode 100644 index 00000000..9319e067 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/entities/EntityContract.kt @@ -0,0 +1,34 @@ +package hs.kr.entrydsm.global.annotation.entities + +/** + * DDD์˜ Entity ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค๊ฐ€ ๋”ฐ๋ผ์•ผ ํ•˜๋Š” ๊ณ„์•ฝ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * Entity๋Š” ๊ณ ์œ ํ•œ ์‹๋ณ„์ž๋ฅผ ๊ฐ€์ง€๋ฉฐ, ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋‚ด์—์„œ + * ์ƒ๋ช…์ฃผ๊ธฐ๋ฅผ ๊ฐ€์ง€๋Š” ๋„๋ฉ”์ธ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.15 + */ +interface EntityContract { + + /** + * ์—”ํ‹ฐํ‹ฐ์˜ ๊ณ ์œ  ์‹๋ณ„์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—”ํ‹ฐํ‹ฐ์˜ ๊ณ ์œ  ์‹๋ณ„์ž + */ + fun getId(): Any + + /** + * ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ + */ + fun getContext(): String + + /** + * ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์†ํ•œ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ ํด๋ž˜์Šค๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ ํด๋ž˜์Šค + */ + fun getAggregateRootClass(): Class<*> +} diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/entities/provider/EntityProvider.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/entities/provider/EntityProvider.kt new file mode 100644 index 00000000..ece76830 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/entities/provider/EntityProvider.kt @@ -0,0 +1,247 @@ +package hs.kr.entrydsm.global.annotation.entities.provider + +import hs.kr.entrydsm.global.annotation.aggregates.provider.AggregateProvider +import hs.kr.entrydsm.global.annotation.entities.Entity +import hs.kr.entrydsm.global.annotation.entities.EntityContract +import hs.kr.entrydsm.global.constants.ErrorCodes +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.global.exception.ValidationException + +/** + * DDD์˜ Entity ํŒจํ„ด ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” Provider ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ์—”ํ‹ฐํ‹ฐ์˜ ๋“ฑ๋ก, ์กฐํšŒ, ๊ฒ€์ฆ์„ ํ†ตํ•ด ๋„๋ฉ”์ธ ์—”ํ‹ฐํ‹ฐ๋“ค์„ + * ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ณ  ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ์™€์˜ ๊ด€๊ณ„๋ฅผ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * ์ฃผ์š” ๊ธฐ๋Šฅ: + * - ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ๋“ฑ๋ก ๋ฐ ๊ด€๋ฆฌ + * - ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ๋ณ„ ์—”ํ‹ฐํ‹ฐ ์กฐํšŒ + * - ์ปจํ…์ŠคํŠธ๋ณ„ ์—”ํ‹ฐํ‹ฐ ์กฐํšŒ + * - ์—”ํ‹ฐํ‹ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ (์–ด๋…ธํ…Œ์ด์…˜, ์ธํ„ฐํŽ˜์ด์Šค, ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ) + * - ์—”ํ‹ฐํ‹ฐ-์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๊ด€๊ณ„ ๊ด€๋ฆฌ + * + * @author kangeunchan + * @since 2025.07.15 + */ +object EntityProvider { + + private val entityRegistry = mutableMapOf, MutableSet>>() + private val entityCache = mutableMapOf, Class<*>>() + private val contextCache = mutableMapOf, String>() + + object ErrorMessages { + const val ENTITY_ANNOTATION_MISSING = "@Entity ์–ด๋…ธํ…Œ์ด์…˜ ํ•„์ˆ˜" + const val ENTITY_CONTRACT_NOT_IMPLEMENTED = "EntityContract ๊ตฌํ˜„ ํ•„์ˆ˜" + const val AGGREGATE_ROOT_INVALID = "Aggregate Root ๋“ฑ๋ก ํ•„์š”" + } + + /** + * ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋กœ ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๋“ฑ๋กํ•  ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ํƒ€์ž… + */ + inline fun registerEntity() { + registerEntity(T::class.java) + } + + /** + * ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityClass ๋“ฑ๋กํ•  ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค + * @param T ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ํƒ€์ž… + * @throws ValidationException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun registerEntity(entityClass: Class) { + validateEntity(entityClass) + val aggregateRoot = getAggregateRootFromAnnotation(entityClass) + val context = getContextFromAnnotation(entityClass) + + entityRegistry.getOrPut(aggregateRoot) { mutableSetOf() }.add(entityClass) + entityCache[entityClass] = aggregateRoot + contextCache[entityClass] = context + } + + /** + * ํŠน์ • ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ์— ์†ํ•œ ๋ชจ๋“  ์—”ํ‹ฐํ‹ฐ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateRoot ๋Œ€์ƒ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ ํด๋ž˜์Šค + * @return ํ•ด๋‹น ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ์— ์†ํ•œ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ง‘ํ•ฉ + */ + fun getEntitiesByAggregate(aggregateRoot: Class<*>): Set> { + return entityRegistry[aggregateRoot] ?: emptySet() + } + + /** + * ํŠน์ • ์ปจํ…์ŠคํŠธ์— ์†ํ•œ ๋ชจ๋“  ์—”ํ‹ฐํ‹ฐ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param context ๋Œ€์ƒ ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ + * @return ํ•ด๋‹น ์ปจํ…์ŠคํŠธ์— ์†ํ•œ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ง‘ํ•ฉ + */ + fun getEntitiesByContext(context: String): Set> { + return contextCache.entries + .filter { it.value == context } + .map { it.key } + .toSet() + } + + /** + * ๋“ฑ๋ก๋œ ๋ชจ๋“  ์—”ํ‹ฐํ‹ฐ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋“ฑ๋ก๋œ ๋ชจ๋“  ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค ์ง‘ํ•ฉ + */ + fun getAllEntities(): Set> { + return entityCache.keys.toSet() + } + + /** + * ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์†ํ•œ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityClass ๋Œ€์ƒ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค + * @return ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์†ํ•œ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ ํด๋ž˜์Šค + * @throws ValidationException @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun getEntityAggregateRoot(entityClass: Class<*>): Class<*> { + return entityCache[entityClass] + ?: getAggregateRootFromAnnotation(entityClass) + } + + /** + * ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityClass ๋Œ€์ƒ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค + * @return ์—”ํ‹ฐํ‹ฐ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ + * @throws IllegalArgumentException @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun getEntityContext(entityClass: Class<*>): String { + return contextCache[entityClass] + ?: getContextFromAnnotation(entityClass) + } + + /** + * ์ฃผ์–ด์ง„ ํด๋ž˜์Šค๊ฐ€ ์—”ํ‹ฐํ‹ฐ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param clazz ํ™•์ธํ•  ํด๋ž˜์Šค + * @return ์—”ํ‹ฐํ‹ฐ์ด๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + */ + fun isEntity(clazz: Class<*>): Boolean { + return entityCache.containsKey(clazz) || hasEntityAnnotation(clazz) + } + + /** + * ๋ชจ๋“  ๋“ฑ๋ก๋œ ์—”ํ‹ฐํ‹ฐ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun clearAll() { + entityRegistry.clear() + entityCache.clear() + contextCache.clear() + } + + /** + * ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityClass ๊ฒ€์ฆํ•  ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค + * @throws IllegalArgumentException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateEntity(entityClass: Class<*>) { + validateEntityAnnotation(entityClass) + validateEntityContract(entityClass) + validateAggregateRoot(entityClass) + } + + /** + * ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค์— @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityClass ๊ฒ€์ฆํ•  ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค + * @throws ValidationException + */ + fun validateEntityAnnotation(entityClass: Class<*>) { + entityClass.getAnnotation(Entity::class.java) + ?: throw ValidationException( + errorCode = ErrorCode.MISSING_ENTITY_ANNOTATION, + field = entityClass.simpleName, + constraint = ErrorMessages.ENTITY_ANNOTATION_MISSING, + message = "ํด๋ž˜์Šค ${entityClass.simpleName}์— @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค." + ) + } + + /** + * ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค๊ฐ€ EntityContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityClass ๊ฒ€์ฆํ•  ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค + * @throws ValidationException + */ + fun validateEntityContract(entityClass: Class<*>) { + if (!EntityContract::class.java.isAssignableFrom(entityClass)) { + throw ValidationException( + errorCode = ErrorCode.ENTITY_CONTRACT_NOT_IMPLEMENTED, + field = entityClass.simpleName, + constraint = ErrorMessages.ENTITY_CONTRACT_NOT_IMPLEMENTED, + message = "ํด๋ž˜์Šค ${entityClass.simpleName}๋Š” EntityContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค." + ) + } + } + + /** + * ์—”ํ‹ฐํ‹ฐ์˜ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityClass ๊ฒ€์ฆํ•  ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค + * @throws IllegalArgumentException ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateAggregateRoot(entityClass: Class<*>) { + val aggregateRoot = getAggregateRootFromAnnotation(entityClass) + if (!AggregateProvider.isAggregate(aggregateRoot)) { + throw ValidationException( + errorCode = ErrorCode.INVALID_AGGREGATE_ROOT, + field = aggregateRoot.simpleName, + constraint = ErrorMessages.AGGREGATE_ROOT_INVALID, + message = "${aggregateRoot.simpleName}์€ ์œ ํšจํ•œ Aggregate Root๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค." + ) + } + } + + /** + * @Entity ์–ด๋…ธํ…Œ์ด์…˜์—์„œ ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ ํด๋ž˜์Šค๋ฅผ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityClass ๋Œ€์ƒ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค + * @return ์• ๊ทธ๋ฆฌ๊ฒŒ์ดํŠธ ๋ฃจํŠธ ํด๋ž˜์Šค + * @throws ValidationException + */ + fun getAggregateRootFromAnnotation(entityClass: Class<*>): Class<*> { + val annotation = entityClass.getAnnotation(Entity::class.java) + ?: throw ValidationException( + errorCode = ErrorCode.MISSING_ENTITY_ANNOTATION, + field = entityClass.simpleName, + constraint = ErrorMessages.ENTITY_ANNOTATION_MISSING, + message = "ํด๋ž˜์Šค ${entityClass.simpleName}์— @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค." + ) + return annotation.aggregateRoot.java + } + + /** + * @Entity ์–ด๋…ธํ…Œ์ด์…˜์—์„œ ์ปจํ…์ŠคํŠธ ์ด๋ฆ„์„ ์ถ”์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * @param entityClass ๋Œ€์ƒ ์—”ํ‹ฐํ‹ฐ ํด๋ž˜์Šค + * @return ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ + * @throws IllegalArgumentException @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun getContextFromAnnotation(entityClass: Class<*>): String { + val annotation = entityClass.getAnnotation(Entity::class.java) + ?: throw ValidationException( + errorCode = ErrorCode.MISSING_ENTITY_ANNOTATION, + field = entityClass.simpleName, + constraint = ErrorMessages.ENTITY_ANNOTATION_MISSING, + message = "ํด๋ž˜์Šค ${entityClass.simpleName}์— @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค." + ) + return annotation.context; + } + + /** + * ํด๋ž˜์Šค์— @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param clazz ํ™•์ธํ•  ํด๋ž˜์Šค + * @return @Entity ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ์œผ๋ฉด true, ์—†์œผ๋ฉด false + */ + fun hasEntityAnnotation(clazz: Class<*>): Boolean { + return clazz.getAnnotation(Entity::class.java) != null + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/Factory.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/Factory.kt new file mode 100644 index 00000000..d89070ba --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/Factory.kt @@ -0,0 +1,27 @@ +package hs.kr.entrydsm.global.annotation.factory + +import hs.kr.entrydsm.global.annotation.factory.type.Complexity + +/** + * DDD(Domain-Driven Design)์˜ Factory ํŒจํ„ด์„ ๋‚˜ํƒ€๋‚ด๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. + * + * Factory๋Š” ๋ณต์žกํ•œ ๊ฐ์ฒด ์ƒ์„ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜์—ฌ ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์ƒ์„ฑ์„ ๋‹ด๋‹นํ•˜๋Š” + * ํด๋ž˜์Šค์ž„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. ํŠนํžˆ Aggregate๋‚˜ Entity์˜ ์ƒ์„ฑ ๊ณผ์ •์ด ๋ณต์žกํ•  ๋•Œ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @param context Factory๊ฐ€ ์†ํ•œ Bounded Context๋ฅผ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. + * @param complexity ๊ฐ์ฒด ์ƒ์„ฑ์˜ ๋ณต์žก๋„๋ฅผ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + * @param cache ์ƒ์„ฑ๋œ ๊ฐ์ฒด๋ฅผ ์บ์‹œํ• ์ง€ ์—ฌ๋ถ€๋ฅผ ๊ฒฐ์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.08 + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Factory( + val context: String, + val complexity: Complexity, + val cache: Boolean +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/FactoryContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/FactoryContract.kt new file mode 100644 index 00000000..635238a3 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/FactoryContract.kt @@ -0,0 +1,37 @@ +package hs.kr.entrydsm.global.annotation.factory + +/** + * DDD์˜ Factory ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค๊ฐ€ ๋”ฐ๋ผ์•ผ ํ•˜๋Š” ๊ณ„์•ฝ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * Factory๋Š” ๋ณต์žกํ•œ ๊ฐ์ฒด ์ƒ์„ฑ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๊ณ , + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์ƒ์„ฑ ์ฑ…์ž„์„ ๋ถ„๋ฆฌํ•˜๋Š” ํŒจํ„ด์ž…๋‹ˆ๋‹ค. + * + * @param T ์ƒ์„ฑํ•  ๊ฐ์ฒด์˜ ํƒ€์ž… + * + * @author kangeunchan + * @since 2025.07.15 + */ +interface FactoryContract { + + /** + * ์ฃผ์–ด์ง„ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param params ๊ฐ์ฒด ์ƒ์„ฑ์— ํ•„์š”ํ•œ ๋งค๊ฐœ๋ณ€์ˆ˜๋“ค + * @return ์ƒ์„ฑ๋œ ๊ฐ์ฒด + */ + fun create(vararg params: Any?): T + + /** + * ํŒฉํ† ๋ฆฌ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํŒฉํ† ๋ฆฌ๊ฐ€ ์†ํ•œ ์ปจํ…์ŠคํŠธ ์ด๋ฆ„ + */ + fun getContext(): String + + /** + * ํŒฉํ† ๋ฆฌ๊ฐ€ ์ƒ์„ฑํ•˜๋Š” ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํด๋ž˜์Šค ํƒ€์ž… + */ + fun getTargetType(): Class +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/provider/FactoryProvider.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/provider/FactoryProvider.kt new file mode 100644 index 00000000..86a27a70 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/provider/FactoryProvider.kt @@ -0,0 +1,247 @@ +package hs.kr.entrydsm.global.annotation.factory.provider + +import hs.kr.entrydsm.global.annotation.factory.Factory +import hs.kr.entrydsm.global.annotation.factory.FactoryContract +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.global.exception.ValidationException + +/** + * DDD์˜ Factory ํŒจํ„ด ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” Provider ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ํŒฉํ† ๋ฆฌ์˜ ๋“ฑ๋ก, ๊ฐ์ฒด ์ƒ์„ฑ, ์ธ์Šคํ„ด์Šค ๊ด€๋ฆฌ, ์บ์‹ฑ์„ ํ†ตํ•ด ๋ณต์žกํ•œ ๊ฐ์ฒด ์ƒ์„ฑ ๋กœ์ง์„ + * ์ค‘์•™ํ™”ํ•˜๊ณ  ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * ์ฃผ์š” ๊ธฐ๋Šฅ: + * - ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค ๋“ฑ๋ก ๋ฐ ๊ด€๋ฆฌ + * - ํƒ€์ž… ์•ˆ์ „ํ•œ ๊ฐ์ฒด ์ƒ์„ฑ + * - ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค ์บ์‹ฑ + * - ์ƒ์„ฑ๋œ ๊ฐ์ฒด ์บ์‹ฑ (์„ ํƒ์ ) + * - ํŒฉํ† ๋ฆฌ ์œ ํšจ์„ฑ ๊ฒ€์ฆ + * + * @author kangeunchan + * @since 2025.07.15 + */ +object FactoryProvider { + + private val factoryCache = mutableMapOf() + private val factoryInstances = mutableMapOf, Any>() + private val factoryRegistry = mutableMapOf, Class<*>>() + + private object ErrorMessages { + const val FACTORY_ANNOTATION_MISSING = "์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค" + const val FACTORY_CONTRACT_NOT_IMPLEMENTED = "์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val FACTORY_NOT_REGISTERED = "์— ๋Œ€ํ•œ ํŒฉํ† ๋ฆฌ๊ฐ€ ๋“ฑ๋ก๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค" + const val CACHE_KEY_REQUIRED = "์บ์‹ฑ์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค" + } + + /** + * ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋กœ ํŒฉํ† ๋ฆฌ๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๋Œ€์ƒ ๊ฐ์ฒด ํƒ€์ž… + * @param F ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค ํƒ€์ž… + */ + inline fun > registerFactory() { + registerFactory(T::class.java, F::class.java) + } + + /** + * ํŒฉํ† ๋ฆฌ๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param targetType ๋Œ€์ƒ ๊ฐ์ฒด ํด๋ž˜์Šค + * @param factoryClass ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค + * @param T ๋Œ€์ƒ ๊ฐ์ฒด ํƒ€์ž… + * @param F ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค ํƒ€์ž… + */ + fun > registerFactory(targetType: Class, factoryClass: Class) { + factoryRegistry[targetType] = factoryClass + } + + /** + * ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋กœ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param cache ์บ์‹ฑ ์‚ฌ์šฉ ์—ฌ๋ถ€ + * @param key ์บ์‹œ ํ‚ค (์บ์‹ฑ ์‚ฌ์šฉ ์‹œ ํ•„์ˆ˜) + * @param params ์ƒ์„ฑ ์‹œ ์ „๋‹ฌํ•  ๋งค๊ฐœ๋ณ€์ˆ˜๋“ค + * @param T ์ƒ์„ฑํ•  ๊ฐ์ฒด ํƒ€์ž… + * @return ์ƒ์„ฑ๋œ ๊ฐ์ฒด + */ + inline fun createObject(cache: Boolean = false, key: String = "", vararg params: Any?): T { + return createObject(T::class.java, cache, key, *params) + } + + /** + * ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋กœ ํŒฉํ† ๋ฆฌ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ํŒฉํ† ๋ฆฌ ํƒ€์ž… + * @return ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค + */ + inline fun getFactory(): T { + return getFactory(T::class.java) + } + + /** + * ์บ์‹œ๋œ ๊ฐ์ฒด๋“ค์„ ๋ชจ๋‘ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun clearCache() { + factoryCache.clear() + } + + /** + * ๋ชจ๋“  ํŒฉํ† ๋ฆฌ ๊ด€๋ จ ๋ฐ์ดํ„ฐ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun clearAll() { + factoryCache.clear() + factoryInstances.clear() + factoryRegistry.clear() + } + + /** + * ์ง€์ •๋œ ํŒฉํ† ๋ฆฌ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param targetType ์ƒ์„ฑํ•  ๊ฐ์ฒด์˜ ํด๋ž˜์Šค + * @param cache ์บ์‹ฑ ์‚ฌ์šฉ ์—ฌ๋ถ€ + * @param key ์บ์‹œ ํ‚ค (์บ์‹ฑ ์‚ฌ์šฉ ์‹œ ํ•„์ˆ˜) + * @param params ์ƒ์„ฑ ์‹œ ์ „๋‹ฌํ•  ๋งค๊ฐœ๋ณ€์ˆ˜๋“ค + * @param T ์ƒ์„ฑํ•  ๊ฐ์ฒด ํƒ€์ž… + * @return ์ƒ์„ฑ๋œ ๊ฐ์ฒด + * @throws IllegalArgumentException ์บ์‹ฑ ์‚ฌ์šฉ ์‹œ ํ‚ค๊ฐ€ ๋น„์–ด์žˆ๊ฑฐ๋‚˜ ํŒฉํ† ๋ฆฌ๊ฐ€ ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + @Suppress("UNCHECKED_CAST") + fun createObject(targetType: Class, cache: Boolean, key: String, vararg params: Any?): T { + val factory = getFactoryForType(targetType) + + return when { + cache && key.isNotEmpty() -> getCachedObject(key) { factory.create(*params) } as T + cache && key.isEmpty() -> throw IllegalArgumentException("์บ์‹ฑ์„ ์‚ฌ์šฉํ•  ๋•Œ๋Š” ํ‚ค๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.") + else -> factory.create(*params) + } + } + + /** + * ์ง€์ •๋œ ํƒ€์ž…์— ๋Œ€ํ•œ ํŒฉํ† ๋ฆฌ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param targetType ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํด๋ž˜์Šค + * @param T ํŒฉํ† ๋ฆฌ ํƒ€์ž… + * @return ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค + * @throws ValidationException ํŒฉํ† ๋ฆฌ๊ฐ€ ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + @Suppress("UNCHECKED_CAST") + fun getFactory(targetType: Class): T { + val factoryClass = getFactoryClass(targetType) + return getFactoryInstance(factoryClass) as T + } + + /** + * ์ง€์ •๋œ ํƒ€์ž…์— ๋Œ€ํ•œ FactoryContract ์ธ์Šคํ„ด์Šค๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param targetType ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํด๋ž˜์Šค + * @param T ๋Œ€์ƒ ๊ฐ์ฒด ํƒ€์ž… + * @return FactoryContract ์ธ์Šคํ„ด์Šค + * @throws ValidationException ํŒฉํ† ๋ฆฌ๊ฐ€ ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + @Suppress("UNCHECKED_CAST") + fun getFactoryForType(targetType: Class): FactoryContract { + val factoryClass = getFactoryClass(targetType) + return getFactoryInstance(factoryClass) as FactoryContract + } + + /** + * ๋“ฑ๋ก๋œ ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param targetType ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํด๋ž˜์Šค + * @return ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค + * @throws ValidationException ํŒฉํ† ๋ฆฌ๊ฐ€ ๋“ฑ๋ก๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun getFactoryClass(targetType: Class<*>): Class<*> { + return factoryRegistry[targetType] + ?: throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "targetType", + value = targetType.simpleName, + message = "${targetType.simpleName}${ErrorMessages.FACTORY_NOT_REGISTERED}." + ) + } + + /** + * ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค๋ฅผ ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param factoryClass ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค + * @return ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค (์บ์‹œ๋จ) + */ + fun getFactoryInstance(factoryClass: Class<*>): Any { + return factoryInstances.getOrPut(factoryClass) { + createFactoryInstance(factoryClass) + } + } + + /** + * ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค์˜ ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param factoryClass ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค + * @return ์ƒ์„ฑ๋œ ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค + * @throws RuntimeException ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ + * @throws ValidationException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun createFactoryInstance(factoryClass: Class<*>): Any { + val instance = factoryClass.getDeclaredConstructor().newInstance() + validateFactory(instance) + return instance + } + + /** + * ์บ์‹œ๋œ ๊ฐ์ฒด๋ฅผ ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ์บ์‹œ ํ‚ค + * @param creator ๊ฐ์ฒด ์ƒ์„ฑ ํ•จ์ˆ˜ + * @return ์บ์‹œ๋œ ๋˜๋Š” ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ๊ฐ์ฒด + */ + fun getCachedObject(key: String, creator: () -> Any): Any { + return factoryCache.getOrPut(key) { creator() } + } + + /** + * ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param factory ๊ฒ€์ฆํ•  ํŒฉํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค + * @throws ValidationException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateFactory(factory: Any) { + val factoryClass = factory::class.java + + validateAnnotation(factoryClass) + validateContract(factoryClass) + } + + /** + * ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค์— @Factory ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param factoryClass ๊ฒ€์ฆํ•  ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค + * @throws ValidationException @Factory ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun validateAnnotation(factoryClass: Class<*>) { + factoryClass.getAnnotation(Factory::class.java) + ?: throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "factoryClass", + value = factoryClass.simpleName, + message = "ํด๋ž˜์Šค ${factoryClass.simpleName}์— @Factory ${ErrorMessages.FACTORY_ANNOTATION_MISSING}." + ) + } + + /** + * ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค๊ฐ€ FactoryContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param factoryClass ๊ฒ€์ฆํ•  ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค + * @throws ValidationException FactoryContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateContract(factoryClass: Class<*>) { + if (!FactoryContract::class.java.isAssignableFrom(factoryClass)) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "factoryClass", + value = factoryClass.simpleName, + message = "ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค ${factoryClass.simpleName}๋Š” FactoryContract ${ErrorMessages.FACTORY_CONTRACT_NOT_IMPLEMENTED}." + ) + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/type/Complexity.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/type/Complexity.kt new file mode 100644 index 00000000..0a13f1cc --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/factory/type/Complexity.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.global.annotation.factory.type + +/** + * Aggregate Root ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์–ด๋…ธํ…Œ์ด์…˜ ์ž…๋‹ˆ๋‹ค. + * + * Aggregate๋Š” ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ์˜ ๋‹จ์œ„๋กœ ์ทจ๊ธ‰๋˜๋Š” ์—ฐ๊ด€๋œ ๊ฐ์ฒด๋“ค์˜ ์ง‘ํ•ฉ ์˜๋ฏธํ•˜๋ฉฐ, + *์ด ์–ด๋…ธํ…Œ์ด์…˜์ด ๋ถ™์€ ํด๋ž˜์Šค๋Š” ํ•ด๋‹น Aggregate์˜ ๋ฃจํŠธ ์—”ํ‹ฐํ‹ฐ์ž„์„ ๋‚˜ํƒ€๋ƒ…๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.08 + * */ +enum class Complexity { + LOW, + NORMAL, + HIGH +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/Policy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/Policy.kt new file mode 100644 index 00000000..d4cce74a --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/Policy.kt @@ -0,0 +1,26 @@ +package hs.kr.entrydsm.global.annotation.policy + +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * DDD(Domain-Driven Design)์˜ ๋น„์ฆˆ๋‹ˆ์Šค ์ •์ฑ…(Business Policy)์„ ๋‚˜ํƒ€๋‚ด๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. + * + * ์ •์ฑ…์€ ๋„๋ฉ”์ธ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์ด๋‚˜ ์ œ์•ฝ์‚ฌํ•ญ์„ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * ์ด ์–ด๋…ธํ…Œ์ด์…˜์„ ํ†ตํ•ด ์ •์ฑ…์˜ ๋‚ด์šฉ, ์ ์šฉ ๋ฒ”์œ„, ์†Œ์† ๋„๋ฉ”์ธ์„ ๋ช…ํ™•ํžˆ ๋ฌธ์„œํ™”ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @param name ์ •์ฑ…์˜ ์ด๋ฆ„ + * @param description ์ •์ฑ…์˜ ์ƒ์„ธ ์„ค๋ช… ๋ฐ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ + * @param domain ์ •์ฑ…์ด ์†ํ•œ ๋„๋ฉ”์ธ(Bounded Context) + * @param scope ์ •์ฑ…์˜ ์ ์šฉ ๋ฒ”์œ„ (GLOBAL, DOMAIN, AGGREGATE, ENTITY) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.08 + */ +annotation class Policy( + val name: String, + val description: String, + val domain: String, + val scope: Scope, +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/PolicyContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/PolicyContract.kt new file mode 100644 index 00000000..ecaf16bd --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/PolicyContract.kt @@ -0,0 +1,66 @@ +package hs.kr.entrydsm.global.annotation.policy + +import hs.kr.entrydsm.global.annotation.policy.type.Scope + +/** + * DDD์˜ ๋น„์ฆˆ๋‹ˆ์Šค ์ •์ฑ…(Business Policy)์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค๊ฐ€ ๋”ฐ๋ผ์•ผ ํ•˜๋Š” ๊ณ„์•ฝ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * ์ •์ฑ…์€ ๋„๋ฉ”์ธ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์ด๋‚˜ ์ œ์•ฝ์‚ฌํ•ญ์„ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋˜๋ฉฐ, + * ์ด ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ํ†ตํ•ด ์ •์ฑ…์˜ ์‹คํ–‰, ๊ฒ€์ฆ, ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ๋ฅผ ํ‘œ์ค€ํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.15 + */ +interface PolicyContract { + + /** + * ์ •์ฑ…์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param context ์ •์ฑ… ์‹คํ–‰์— ํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ ์ •๋ณด + * @return ์ •์ฑ… ์‹คํ–‰ ๊ฒฐ๊ณผ + */ + fun execute(context: Map): PolicyResult + + /** + * ์ •์ฑ… ์ ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param context ๊ฒ€์ฆ์— ํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ ์ •๋ณด + * @return ์ •์ฑ… ์ ์šฉ ๊ฐ€๋Šฅ ์—ฌ๋ถ€ + */ + fun isApplicable(context: Map): Boolean + + /** + * ์ •์ฑ…์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ •์ฑ…์˜ ์ด๋ฆ„ + */ + fun getName(): String + + /** + * ์ •์ฑ…์˜ ์„ค๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ •์ฑ…์˜ ์ƒ์„ธ ์„ค๋ช… + */ + fun getDescription(): String + + /** + * ์ •์ฑ…์ด ์†ํ•œ ๋„๋ฉ”์ธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ •์ฑ…์ด ์†ํ•œ ๋„๋ฉ”์ธ๋ช… + */ + fun getDomain(): String + + /** + * ์ •์ฑ…์˜ ์ ์šฉ ๋ฒ”์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ •์ฑ…์˜ ์ ์šฉ ๋ฒ”์œ„ (GLOBAL, DOMAIN, AGGREGATE, ENTITY) + */ + fun getScope(): Scope + + /** + * ์ •์ฑ…์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ •์ฑ…์˜ ์šฐ์„ ์ˆœ์œ„ (์ˆซ์ž๊ฐ€ ๋‚ฎ์„์ˆ˜๋ก ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + */ + fun getPriority(): Int = Int.MAX_VALUE +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/PolicyResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/PolicyResult.kt new file mode 100644 index 00000000..6ee89a4f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/PolicyResult.kt @@ -0,0 +1,14 @@ +package hs.kr.entrydsm.global.annotation.policy + +/** + * ์ •์ฑ… ์‹คํ–‰ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @param success ์ •์ฑ… ์‹คํ–‰ ์„ฑ๊ณต ์—ฌ๋ถ€ + * @param message ์‹คํ–‰ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @param data ์‹คํ–‰ ๊ฒฐ๊ณผ ๋ฐ์ดํ„ฐ + */ +data class PolicyResult( + val success: Boolean, + val message: String = "", + val data: Map = emptyMap() +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/provider/PolicyProvider.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/provider/PolicyProvider.kt new file mode 100644 index 00000000..068c5c65 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/provider/PolicyProvider.kt @@ -0,0 +1,306 @@ +package hs.kr.entrydsm.global.annotation.policy.provider + +import hs.kr.entrydsm.global.annotation.policy.Policy +import hs.kr.entrydsm.global.annotation.policy.PolicyContract +import hs.kr.entrydsm.global.annotation.policy.PolicyResult +import hs.kr.entrydsm.global.annotation.policy.type.Scope +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.global.exception.ValidationException + +/** + * DDD์˜ ๋น„์ฆˆ๋‹ˆ์Šค ์ •์ฑ…(Business Policy) ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” Provider ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ์ •์ฑ…์˜ ๋“ฑ๋ก, ์‹คํ–‰, ๊ฒ€์ƒ‰, ๊ฒ€์ฆ์„ ํ†ตํ•ด ๋„๋ฉ”์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ + * ๋ช…์‹œ์ ์ด๊ณ  ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.15 + */ +object PolicyProvider { + + private val policyRegistry = mutableMapOf>>() + private val policyInstances = mutableMapOf, PolicyContract>() + private val scopeRegistry = mutableMapOf>>() + private val domainRegistry = mutableMapOf>>() + private val policyCache = mutableMapOf() + + private object ErrorMessages { + const val POLICY_ANNOTATION_MISSING = "์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค" + const val POLICY_CONTRACT_NOT_IMPLEMENTED = "์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val POLICY_NOT_FOUND = "์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val MULTIPLE_IMPLEMENTATIONS = "์— ๋Œ€ํ•ด ์—ฌ๋Ÿฌ ๊ตฌํ˜„์ฒด๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค" + const val POLICY_NOT_APPLICABLE = "์€ ํ˜„์žฌ ์ปจํ…์ŠคํŠธ์— ์ ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + } + + /** + * ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋กœ ์ •์ฑ…์„ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๋“ฑ๋กํ•  ์ •์ฑ… ํด๋ž˜์Šค ํƒ€์ž… + */ + inline fun registerPolicy() { + registerPolicy(T::class.java) + } + + /** + * ์ •์ฑ… ํด๋ž˜์Šค๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param policyClass ๋“ฑ๋กํ•  ์ •์ฑ… ํด๋ž˜์Šค + * @param T ์ •์ฑ… ํด๋ž˜์Šค ํƒ€์ž… + * @throws ValidationException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun registerPolicy(policyClass: Class) { + validatePolicy(policyClass) + + val annotation = getPolicyAnnotation(policyClass) + val name = annotation.name + val domain = annotation.domain + val scope = annotation.scope + + policyRegistry.getOrPut(name) { mutableSetOf() }.add(policyClass) + scopeRegistry.getOrPut(scope) { mutableSetOf() }.add(policyClass) + domainRegistry.getOrPut(domain) { mutableSetOf() }.add(policyClass) + } + + /** + * ํŠน์ • ์ด๋ฆ„์˜ ์ •์ฑ…์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์‹คํ–‰ํ•  ์ •์ฑ…์˜ ์ด๋ฆ„ + * @param context ์ •์ฑ… ์‹คํ–‰์— ํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ ์ •๋ณด + * @return ์ •์ฑ… ์‹คํ–‰ ๊ฒฐ๊ณผ + * @throws ValidationException ์ •์ฑ…์„ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun executePolicy(name: String, context: Map): PolicyResult { + val policy = getPolicyByName(name) + + return if (policy.isApplicable(context)) { + policy.execute(context) + } else { + PolicyResult( + success = false, + message = "์ •์ฑ… '$name'${ErrorMessages.POLICY_NOT_APPLICABLE}." + ) + } + } + + /** + * ํŠน์ • ๋ฒ”์œ„(Scope)์— ์†ํ•œ ๋ชจ๋“  ์ •์ฑ…๋“ค์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param scope ๋Œ€์ƒ ๋ฒ”์œ„ (GLOBAL, DOMAIN, AGGREGATE, ENTITY) + * @param context ์ •์ฑ… ์‹คํ–‰์— ํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ ์ •๋ณด + * @return ๊ฐ ์ •์ฑ…์— ๋Œ€ํ•œ ์‹คํ–‰ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ (์šฐ์„ ์ˆœ์œ„ ์˜ค๋ฆ„์ฐจ์ˆœ) + */ + fun executePoliciesByScope(scope: Scope, context: Map): List { + return getPoliciesByScope(scope) + .sortedBy { it.getPriority() } + .map { policy -> + if (policy.isApplicable(context)) { + policy.execute(context) + } else { + PolicyResult( + success = false, + message = "์ •์ฑ… '${policy.getName()}'${ErrorMessages.POLICY_NOT_APPLICABLE}." + ) + } + } + } + + /** + * ํŠน์ • ๋„๋ฉ”์ธ์— ์†ํ•œ ๋ชจ๋“  ์ •์ฑ…๋“ค์„ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param domain ๋Œ€์ƒ ๋„๋ฉ”์ธ ์ด๋ฆ„ + * @param context ์ •์ฑ… ์‹คํ–‰์— ํ•„์š”ํ•œ ์ปจํ…์ŠคํŠธ ์ •๋ณด + * @return ๊ฐ ์ •์ฑ…์— ๋Œ€ํ•œ ์‹คํ–‰ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ (์šฐ์„ ์ˆœ์œ„ ์˜ค๋ฆ„์ฐจ์ˆœ) + */ + fun executePoliciesByDomain(domain: String, context: Map): List { + return getPoliciesByDomain(domain) + .sortedBy { it.getPriority() } + .map { policy -> + if (policy.isApplicable(context)) { + policy.execute(context) + } else { + PolicyResult( + success = false, + message = "์ •์ฑ… '${policy.getName()}'${ErrorMessages.POLICY_NOT_APPLICABLE}." + ) + } + } + } + + /** + * ์ด๋ฆ„์„ ํ†ตํ•ด ์ •์ฑ…์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์กฐํšŒํ•  ์ •์ฑ…์˜ ์ด๋ฆ„ + * @return ์ •์ฑ… ์ธ์Šคํ„ด์Šค + * @throws ValidationException ์ •์ฑ…์„ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ์ค‘๋ณต ๊ตฌํ˜„์ฒด๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ + */ + fun getPolicyByName(name: String): PolicyContract { + return policyCache.getOrPut(name) { + val policyClasses = policyRegistry[name] + ?: throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "policyName", + value = name, + message = "์ •์ฑ… '$name'${ErrorMessages.POLICY_NOT_FOUND}." + ) + + if (policyClasses.size > 1) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "policyName", + value = name, + message = "์ •์ฑ… '$name'${ErrorMessages.MULTIPLE_IMPLEMENTATIONS}: ${policyClasses.map { it.simpleName }}" + ) + } + + getPolicyInstance(policyClasses.first()) + } + } + + /** + * ํŠน์ • ๋ฒ”์œ„(Scope)์— ์†ํ•œ ๋ชจ๋“  ์ •์ฑ…๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param scope ๋Œ€์ƒ ๋ฒ”์œ„ (GLOBAL, DOMAIN, AGGREGATE, ENTITY) + * @return ํ•ด๋‹น ๋ฒ”์œ„์— ์†ํ•œ ์ •์ฑ… ๋ฆฌ์ŠคํŠธ + */ + fun getPoliciesByScope(scope: Scope): List { + return scopeRegistry[scope]?.map { getPolicyInstance(it) } ?: emptyList() + } + + /** + * ํŠน์ • ๋„๋ฉ”์ธ์— ์†ํ•œ ๋ชจ๋“  ์ •์ฑ…๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param domain ๋Œ€์ƒ ๋„๋ฉ”์ธ ์ด๋ฆ„ + * @return ๋„๋ฉ”์ธ์— ์†ํ•œ ์ •์ฑ… ๋ฆฌ์ŠคํŠธ + */ + fun getPoliciesByDomain(domain: String): List { + return domainRegistry[domain]?.map { getPolicyInstance(it) } ?: emptyList() + } + + /** + * ๋“ฑ๋ก๋œ ๋ชจ๋“  ์ •์ฑ…๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋“ฑ๋ก๋œ ๋ชจ๋“  ์ •์ฑ… ๋ฆฌ์ŠคํŠธ + */ + fun getAllPolicies(): List { + return policyRegistry.values + .flatten() + .distinct() + .map { getPolicyInstance(it) } + } + + /** + * ์ฃผ์–ด์ง„ ํด๋ž˜์Šค๊ฐ€ ์œ ํšจํ•œ ์ •์ฑ… ํด๋ž˜์Šค์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param clazz ํ™•์ธํ•  ํด๋ž˜์Šค + * @return ์œ ํšจํ•œ ์ •์ฑ… ํด๋ž˜์Šค์ด๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + */ + fun isPolicy(clazz: Class<*>): Boolean { + return hasPolicyAnnotation(clazz) && PolicyContract::class.java.isAssignableFrom(clazz) + } + + /** + * ๋ชจ๋“  ๋“ฑ๋ก๋œ ์ •์ฑ…๊ณผ ์บ์‹œ๋ฅผ ์ง€์›๋‹ˆ๋‹ค. + */ + fun clearAll() { + policyRegistry.clear() + policyInstances.clear() + scopeRegistry.clear() + domainRegistry.clear() + policyCache.clear() + } + + /** + * ์ •์ฑ… ํด๋ž˜์Šค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param policyClass ๊ฒ€์ฆํ•  ์ •์ฑ… ํด๋ž˜์Šค + * @throws ValidationException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validatePolicy(policyClass: Class<*>) { + validatePolicyAnnotation(policyClass) + validatePolicyContract(policyClass) + } + + /** + * ์ •์ฑ… ํด๋ž˜์Šค์— @Policy ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param policyClass ๊ฒ€์ฆํ•  ์ •์ฑ… ํด๋ž˜์Šค + * @throws ValidationException @Policy ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun validatePolicyAnnotation(policyClass: Class<*>) { + policyClass.getAnnotation(Policy::class.java) + ?: throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "policyClass", + value = policyClass.simpleName, + message = "ํด๋ž˜์Šค ${policyClass.simpleName}์— @Policy ${ErrorMessages.POLICY_ANNOTATION_MISSING}." + ) + } + + /** + * ์ •์ฑ… ํด๋ž˜์Šค๊ฐ€ PolicyContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param policyClass ๊ฒ€์ฆํ•  ์ •์ฑ… ํด๋ž˜์Šค + * @throws ValidationException PolicyContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validatePolicyContract(policyClass: Class<*>) { + if (!PolicyContract::class.java.isAssignableFrom(policyClass)) { + throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "policyClass", + value = policyClass.simpleName, + message = "ํด๋ž˜์Šค ${policyClass.simpleName}๋Š” PolicyContract ${ErrorMessages.POLICY_CONTRACT_NOT_IMPLEMENTED}." + ) + } + } + + /** + * ์ •์ฑ… ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param policyClass ๋Œ€์ƒ ์ •์ฑ… ํด๋ž˜์Šค + * @return ์ •์ฑ… ์ธ์Šคํ„ด์Šค (์บ์‹œ๋จ) + */ + fun getPolicyInstance(policyClass: Class<*>): PolicyContract { + return policyInstances.getOrPut(policyClass) { + createPolicyInstance(policyClass) + } + } + + /** + * ์ •์ฑ… ํด๋ž˜์Šค์˜ ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param policyClass ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•  ์ •์ฑ… ํด๋ž˜์Šค + * @return ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ์ •์ฑ… ์ธ์Šคํ„ด์Šค + * @throws RuntimeException ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ + */ + @Suppress("UNCHECKED_CAST") + fun createPolicyInstance(policyClass: Class<*>): PolicyContract { + return policyClass.getDeclaredConstructor().newInstance() as PolicyContract + } + + /** + * ์ •์ฑ… ํด๋ž˜์Šค์—์„œ @Policy ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @param policyClass ๋Œ€์ƒ ์ •์ฑ… ํด๋ž˜์Šค + * @return @Policy ์–ด๋…ธํ…Œ์ด์…˜ ์ธ์Šคํ„ด์Šค + * @throws ValidationException @Policy ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun getPolicyAnnotation(policyClass: Class<*>): Policy { + return policyClass.getAnnotation(Policy::class.java) + ?: throw ValidationException( + errorCode = ErrorCode.VALIDATION_FAILED, + field = "policyClass", + value = policyClass.simpleName, + message = "ํด๋ž˜์Šค ${policyClass.simpleName}์— @Policy ${ErrorMessages.POLICY_ANNOTATION_MISSING}." + ) + } + + /** + * ํด๋ž˜์Šค์— @Policy ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param clazz ํ™•์ธํ•  ํด๋ž˜์Šค + * @return @Policy ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ์œผ๋ฉด true, ์—†์œผ๋ฉด false + */ + fun hasPolicyAnnotation(clazz: Class<*>): Boolean { + return clazz.getAnnotation(Policy::class.java) != null + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/type/Scope.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/type/Scope.kt new file mode 100644 index 00000000..27001cf9 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/policy/type/Scope.kt @@ -0,0 +1,24 @@ +package hs.kr.entrydsm.global.annotation.policy.type + +/** + * ์ •์ฑ…(Policy)์˜ ์ ์šฉ ๋ฒ”์œ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + * + * DDD์—์„œ ๋น„์ฆˆ๋‹ˆ์Šค ์ •์ฑ…์ด๋‚˜ ๋„๋ฉ”์ธ ๊ทœ์น™์ด ์–ด๋А ๋ ˆ๋ฒจ์—์„œ ์ ์šฉ๋˜๋Š”์ง€๋ฅผ + * ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.08 + */ +enum class Scope { + /** ์ „์—ญ ๋ฒ”์œ„ - ์‹œ์Šคํ…œ ์ „์ฒด์— ์ ์šฉ๋˜๋Š” ์ •์ฑ… */ + GLOBAL, + + /** ๋„๋ฉ”์ธ ๋ฒ”์œ„ - ํŠน์ • ๋„๋ฉ”์ธ(Bounded Context) ๋‚ด์—์„œ๋งŒ ์ ์šฉ๋˜๋Š” ์ •์ฑ… */ + DOMAIN, + + /** ์ง‘ํ•ฉ์ฒด ๋ฒ”์œ„ - ํŠน์ • Aggregate ๋‚ด์—์„œ๋งŒ ์ ์šฉ๋˜๋Š” ์ •์ฑ… */ + AGGREGATE, + + /** ์—”ํ‹ฐํ‹ฐ ๋ฒ”์œ„ - ํŠน์ • Entity์—์„œ๋งŒ ์ ์šฉ๋˜๋Š” ์ •์ฑ… */ + ENTITY +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/service/Service.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/service/Service.kt new file mode 100644 index 00000000..d1598c78 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/service/Service.kt @@ -0,0 +1,25 @@ +package hs.kr.entrydsm.global.annotation.service + +import hs.kr.entrydsm.global.annotation.service.type.ServiceType + +/** + * ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋ฅผ ํ‘œ์‹œํ•˜๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. + * + * ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋Š” ์—ฌ๋Ÿฌ ์—”ํ‹ฐํ‹ฐ๋‚˜ ๊ฐ’ ๊ฐ์ฒด์— ๊ฑธ์ณ ์žˆ๋Š” ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๋ฉฐ, + * ํŠน์ • ์—”ํ‹ฐํ‹ฐ์— ์†ํ•˜์ง€ ์•Š๋Š” ๋„๋ฉ”์ธ ์—ฐ์‚ฐ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์„œ๋น„์Šค์˜ ์ด๋ฆ„ (์„ค๋ช…) + * @param type ์„œ๋น„์Šค์˜ ํƒ€์ž… (๋„๋ฉ”์ธ ์„œ๋น„์Šค, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค ๋“ฑ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.21 + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Service( + val name: String, + val type: ServiceType +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/service/type/ServiceType.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/service/type/ServiceType.kt new file mode 100644 index 00000000..c1ce1d95 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/service/type/ServiceType.kt @@ -0,0 +1,39 @@ +package hs.kr.entrydsm.global.annotation.service.type + +/** + * ์„œ๋น„์Šค์˜ ํƒ€์ž…์„ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + * + * ๋„๋ฉ”์ธ ์ฃผ๋„ ์„ค๊ณ„์—์„œ ์„œ๋น„์Šค๋Š” ํฌ๊ฒŒ ๋„๋ฉ”์ธ ์„œ๋น„์Šค์™€ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค๋กœ ๊ตฌ๋ถ„๋ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.21 + */ +enum class ServiceType( + val description: String +) { + /** + * ๋„๋ฉ”์ธ ์„œ๋น„์Šค + * + * ๋„๋ฉ”์ธ ๋กœ์ง์„ ํฌํ•จํ•˜๋ฉฐ, ํŠน์ • ์—”ํ‹ฐํ‹ฐ์— ์†ํ•˜์ง€ ์•Š๋Š” + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ๊ตฌํ˜„ํ•ฉ๋‹ˆ๋‹ค. + */ + DOMAIN_SERVICE("๋„๋ฉ”์ธ ์„œ๋น„์Šค"), + + /** + * ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค + * + * ์œ ์Šค์ผ€์ด์Šค๋ฅผ ์กฐ์œจํ•˜๋ฉฐ, ๋„๋ฉ”์ธ ๊ฐ์ฒด๋“ค ๊ฐ„์˜ ํ˜‘๋ ฅ์„ + * ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + APPLICATION_SERVICE("์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์„œ๋น„์Šค"), + + /** + * ์ธํ”„๋ผ์ŠคํŠธ๋Ÿญ์ฒ˜ ์„œ๋น„์Šค + * + * ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ์˜ ์—ฐ๋™์ด๋‚˜ ๊ธฐ์ˆ ์  ๊ด€์‹ฌ์‚ฌ๋ฅผ + * ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + */ + INFRASTRUCTURE_SERVICE("์ธํ”„๋ผ์ŠคํŠธ๋Ÿญ์ฒ˜ ์„œ๋น„์Šค") +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/AndSpecification.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/AndSpecification.kt new file mode 100644 index 00000000..af95a20a --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/AndSpecification.kt @@ -0,0 +1,76 @@ +package hs.kr.entrydsm.global.annotation.specification + +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * AND ์กฐ๊ฑด์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ณตํ•ฉ ๋ช…์„ธ์ž…๋‹ˆ๋‹ค. + * + * ๋‘ ๊ฐœ์˜ ๋ช…์„ธ๊ฐ€ ๋ชจ๋‘ ๋งŒ์กฑ๋˜์–ด์•ผ ์ „์ฒด ๋ช…์„ธ๊ฐ€ ๋งŒ์กฑ๋˜๋Š” ์กฐ๊ฑด์„ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๊ฒ€์ฆ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @param left ์ฒซ ๋ฒˆ์งธ ๋ช…์„ธ + * @param right ๋‘ ๋ฒˆ์งธ ๋ช…์„ธ + * + * @author kangeunchan + * @since 2025.07.15 + */ +internal class AndSpecification( + private val left: SpecificationContract, + private val right: SpecificationContract +) : SpecificationContract { + + /** + * ๋‘ ๋ช…์„ธ๊ฐ€ ๋ชจ๋‘ ๋งŒ์กฑ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param candidate ๊ฒ€์ฆํ•  ๊ฐ์ฒด + * @return ๋‘ ๋ช…์„ธ ๋ชจ๋‘ ๋งŒ์กฑํ•˜๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + */ + override fun isSatisfiedBy(candidate: T): Boolean { + return left.isSatisfiedBy(candidate) && right.isSatisfiedBy(candidate) + } + + /** + * ๊ฒฐํ•ฉ๋œ ๋ช…์„ธ์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "[left] AND [right]" ํ˜•ํƒœ์˜ ๋ช…์„ธ ์ด๋ฆ„ + */ + override fun getName(): String = "${left.getName()} AND ${right.getName()}" + + /** + * ๊ฒฐํ•ฉ๋œ ๋ช…์„ธ์˜ ์„ค๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "[left] AND [right]" ํ˜•ํƒœ์˜ ๋ช…์„ธ ์„ค๋ช… + */ + override fun getDescription(): String = "${left.getDescription()} AND ${right.getDescription()}" + + /** + * ์ฒซ ๋ฒˆ์งธ ๋ช…์„ธ์˜ ๋„๋ฉ”์ธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ์ด๋ฆ„ + */ + override fun getDomain(): String = left.getDomain() + + /** + * ๋‘ ๋ช…์„ธ ์ค‘ ๋” ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‘ ๋ช…์„ธ์˜ ์šฐ์„ ์ˆœ์œ„ ์ค‘ ์ตœ๋Œ€๊ฐ’ + */ + override fun getPriority(): Priority = maxOf(left.getPriority(), right.getPriority()) + + /** + * ์‹คํŒจํ•œ ๋ช…์„ธ๋“ค์˜ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ AND๋กœ ๊ฒฐํ•ฉํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param candidate ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ๊ฐ์ฒด + * @return ์‹คํŒจํ•œ ๋ช…์„ธ๋“ค์˜ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ AND๋กœ ๊ฒฐํ•ฉํ•œ ๋ฌธ์ž์—ด + */ + override fun getErrorMessage(candidate: T): String { + val errors = mutableListOf() + if (!left.isSatisfiedBy(candidate)) { + errors.add(left.getErrorMessage(candidate)) + } + if (!right.isSatisfiedBy(candidate)) { + errors.add(right.getErrorMessage(candidate)) + } + return errors.joinToString(" AND ") + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/NotSpecification.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/NotSpecification.kt new file mode 100644 index 00000000..01f0c7bc --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/NotSpecification.kt @@ -0,0 +1,67 @@ +package hs.kr.entrydsm.global.annotation.specification + +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * NOT ์กฐ๊ฑด์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ณตํ•ฉ ๋ช…์„ธ์ž…๋‹ˆ๋‹ค. + * + * ๊ธฐ์กด ๋ช…์„ธ์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ถ€์ •(NOT)ํ•˜์—ฌ ๋ฐ˜๋Œ€ ์กฐ๊ฑด์„ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๊ฒ€์ฆ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @param specification ๋ถ€์ •ํ•  ๋Œ€์ƒ ๋ช…์„ธ + * + * @author kangeunchan + * @since 2025.07.15 + */ +internal class NotSpecification( + private val specification: SpecificationContract +) : SpecificationContract { + + /** + * ๋Œ€์ƒ ๋ช…์„ธ์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ถ€์ •ํ•˜์—ฌ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param candidate ๊ฒ€์ฆํ•  ๊ฐ์ฒด + * @return ๋Œ€์ƒ ๋ช…์„ธ๊ฐ€ ์‹คํŒจํ•˜๋ฉด true, ์„ฑ๊ณตํ•˜๋ฉด false + */ + override fun isSatisfiedBy(candidate: T): Boolean { + return !specification.isSatisfiedBy(candidate) + } + + /** + * ๋ถ€์ •๋œ ๋ช…์„ธ์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "NOT [specification]" ํ˜•ํƒœ์˜ ๋ช…์„ธ ์ด๋ฆ„ + */ + override fun getName(): String = "NOT ${specification.getName()}" + + /** + * ๋ถ€์ •๋œ ๋ช…์„ธ์˜ ์„ค๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "NOT [specification]" ํ˜•ํƒœ์˜ ๋ช…์„ธ ์„ค๋ช… + */ + override fun getDescription(): String = "NOT ${specification.getDescription()}" + + /** + * ๋Œ€์ƒ ๋ช…์„ธ์˜ ๋„๋ฉ”์ธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ์ด๋ฆ„ + */ + override fun getDomain(): String = specification.getDomain() + + /** + * ๋Œ€์ƒ ๋ช…์„ธ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋Œ€์ƒ ๋ช…์„ธ์˜ ์šฐ์„ ์ˆœ์œ„ + */ + override fun getPriority(): Priority = specification.getPriority() + + /** + * NOT ๋ช…์„ธ์— ๋Œ€ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param candidate ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ๊ฐ์ฒด + * @return NOT ๋ช…์„ธ ์‹คํŒจ์— ๋Œ€ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + */ + override fun getErrorMessage(candidate: T): String { + return "๊ฐ์ฒด๊ฐ€ ๋ช…์„ธ '${getName()}'๋ฅผ ๋งŒ์กฑํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/OrSpecification.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/OrSpecification.kt new file mode 100644 index 00000000..326bf0b2 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/OrSpecification.kt @@ -0,0 +1,69 @@ +package hs.kr.entrydsm.global.annotation.specification + +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * OR ์กฐ๊ฑด์„ ๊ตฌํ˜„ํ•˜๋Š” ๋ณตํ•ฉ ๋ช…์„ธ์ž…๋‹ˆ๋‹ค. + * + * ๋‘ ๊ฐœ์˜ ๋ช…์„ธ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋งŒ์กฑ๋˜๋ฉด ์ „์ฒด ๋ช…์„ธ๊ฐ€ ๋งŒ์กฑ๋˜๋Š” ์กฐ๊ฑด์„ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๊ฒ€์ฆ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @param left ์ฒซ ๋ฒˆ์งธ ๋ช…์„ธ + * @param right ๋‘ ๋ฒˆ์งธ ๋ช…์„ธ + * + * @author kangeunchan + * @since 2025.07.15 + */ +internal class OrSpecification( + private val left: SpecificationContract, + private val right: SpecificationContract +) : SpecificationContract { + + /** + * ๋‘ ๋ช…์„ธ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋งŒ์กฑ๋˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param candidate ๊ฒ€์ฆํ•  ๊ฐ์ฒด + * @return ๋‘ ๋ช…์„ธ ์ค‘ ํ•˜๋‚˜๋ผ๋„ ๋งŒ์กฑํ•˜๋ฉด true, ๋ชจ๋‘ ์‹คํŒจํ•˜๋ฉด false + */ + override fun isSatisfiedBy(candidate: T): Boolean { + return left.isSatisfiedBy(candidate) || right.isSatisfiedBy(candidate) + } + + /** + * ๊ฒฐํ•ฉ๋œ ๋ช…์„ธ์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "[left] OR [right]" ํ˜•ํƒœ์˜ ๋ช…์„ธ ์ด๋ฆ„ + */ + override fun getName(): String = "${left.getName()} OR ${right.getName()}" + + /** + * ๊ฒฐํ•ฉ๋œ ๋ช…์„ธ์˜ ์„ค๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "[left] OR [right]" ํ˜•ํƒœ์˜ ๋ช…์„ธ ์„ค๋ช… + */ + override fun getDescription(): String = "${left.getDescription()} OR ${right.getDescription()}" + + /** + * ์ฒซ ๋ฒˆ์งธ ๋ช…์„ธ์˜ ๋„๋ฉ”์ธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ์ด๋ฆ„ + */ + override fun getDomain(): String = left.getDomain() + + /** + * ๋‘ ๋ช…์„ธ ์ค‘ ๋” ๋†’์€ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‘ ๋ช…์„ธ์˜ ์šฐ์„ ์ˆœ์œ„ ์ค‘ ์ตœ๋Œ€๊ฐ’ + */ + override fun getPriority(): Priority = maxOf(left.getPriority(), right.getPriority()) + + /** + * OR ๋ช…์„ธ์— ๋Œ€ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param candidate ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ๊ฐ์ฒด + * @return OR ๋ช…์„ธ ์‹คํŒจ์— ๋Œ€ํ•œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + */ + override fun getErrorMessage(candidate: T): String { + return "๊ฐ์ฒด๊ฐ€ ๋ช…์„ธ '${getName()}'๋ฅผ ๋งŒ์กฑํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/Specification.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/Specification.kt new file mode 100644 index 00000000..09df14f7 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/Specification.kt @@ -0,0 +1,29 @@ +package hs.kr.entrydsm.global.annotation.specification + +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * DDD(Domain-Driven Design)์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™(Business Specification)์„ ๋‚˜ํƒ€๋‚ด๋Š” ์–ด๋…ธํ…Œ์ด์…˜์ž…๋‹ˆ๋‹ค. + * + * ๊ทœ์น™์€ ๋„๋ฉ”์ธ์˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด๋‚˜ ๊ฒ€์ฆ ์กฐ๊ฑด์„ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * Specification ํŒจํ„ด๊ณผ ํ•จ๊ป˜ ์‚ฌ์šฉ๋˜์–ด ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ์กฐ๊ฑด์„ ๊ตฌ์กฐํ™”ํ•˜๊ณ  ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“ญ๋‹ˆ๋‹ค. + * + * @param name ๊ทœ์น™์˜ ์ด๋ฆ„ + * @param description ๊ทœ์น™์˜ ์ƒ์„ธ ์„ค๋ช… ๋ฐ ์ ์šฉ ์กฐ๊ฑด + * @param domain ๊ทœ์น™์ด ์†ํ•œ ๋„๋ฉ”์ธ(Bounded Context) + * @param priority ๊ทœ์น™์˜ ์šฐ์„ ์ˆœ์œ„ (LOW, NORMAL, HIGH) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.08 + */ +@Target(AnnotationTarget.CLASS) +@Retention(AnnotationRetention.RUNTIME) +@MustBeDocumented +annotation class Specification( + val name: String, + val description: String, + val domain: String, + val priority: Priority, +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/SpecificationContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/SpecificationContract.kt new file mode 100644 index 00000000..70f2e61f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/SpecificationContract.kt @@ -0,0 +1,90 @@ +package hs.kr.entrydsm.global.annotation.specification + +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * DDD์˜ Specification ํŒจํ„ด์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค๊ฐ€ ๋”ฐ๋ผ์•ผ ํ•˜๋Š” ๊ณ„์•ฝ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * Specification์€ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์ด๋‚˜ ์กฐ๊ฑด์„ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๊ณ , + * ๋ณต์žกํ•œ ๋„๋ฉ”์ธ ๋กœ์ง์„ ๊ตฌ์กฐํ™”ํ•˜์—ฌ ์žฌ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ํŒจํ„ด์ž…๋‹ˆ๋‹ค. + * + * @param T ๊ฒ€์ฆ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * + * @author kangeunchan + * @since 2025.07.15 + */ +interface SpecificationContract { + + /** + * ์ฃผ์–ด์ง„ ๊ฐ์ฒด๊ฐ€ ์ด ๋ช…์„ธ๋ฅผ ๋งŒ์กฑํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param candidate ๊ฒ€์ฆํ•  ๊ฐ์ฒด + * @return ๋ช…์„ธ๋ฅผ ๋งŒ์กฑํ•˜๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + */ + fun isSatisfiedBy(candidate: T): Boolean + + /** + * ์ด ๋ช…์„ธ์™€ ๋‹ค๋ฅธ ๋ช…์„ธ๋ฅผ AND ์กฐ๊ฑด์œผ๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๊ฒฐํ•ฉํ•  ๋‹ค๋ฅธ ๋ช…์„ธ + * @return ๋‘ ๋ช…์„ธ๋ฅผ AND๋กœ ๊ฒฐํ•ฉํ•œ ์ƒˆ๋กœ์šด ๋ช…์„ธ + */ + fun and(other: SpecificationContract): SpecificationContract { + return AndSpecification(this, other) + } + + /** + * ์ด ๋ช…์„ธ์™€ ๋‹ค๋ฅธ ๋ช…์„ธ๋ฅผ OR ์กฐ๊ฑด์œผ๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๊ฒฐํ•ฉํ•  ๋‹ค๋ฅธ ๋ช…์„ธ + * @return ๋‘ ๋ช…์„ธ๋ฅผ OR๋กœ ๊ฒฐํ•ฉํ•œ ์ƒˆ๋กœ์šด ๋ช…์„ธ + */ + fun or(other: SpecificationContract): SpecificationContract { + return OrSpecification(this, other) + } + + /** + * ์ด ๋ช…์„ธ๋ฅผ ๋ถ€์ •(NOT)ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ด ๋ช…์„ธ๋ฅผ ๋ถ€์ •ํ•œ ์ƒˆ๋กœ์šด ๋ช…์„ธ + */ + fun not(): SpecificationContract { + return NotSpecification(this) + } + + /** + * ๋ช…์„ธ์˜ ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ช…์„ธ์˜ ์ด๋ฆ„ + */ + fun getName(): String + + /** + * ๋ช…์„ธ์˜ ์„ค๋ช…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ช…์„ธ์˜ ์ƒ์„ธ ์„ค๋ช… + */ + fun getDescription(): String + + /** + * ๋ช…์„ธ๊ฐ€ ์†ํ•œ ๋„๋ฉ”์ธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ช…์„ธ๊ฐ€ ์†ํ•œ ๋„๋ฉ”์ธ๋ช… + */ + fun getDomain(): String + + /** + * ๋ช…์„ธ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ช…์„ธ์˜ ์šฐ์„ ์ˆœ์œ„ (LOW, NORMAL, HIGH) + */ + fun getPriority(): Priority + + /** + * ๋ช…์„ธ ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์„ ๋•Œ์˜ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param candidate ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ๊ฐ์ฒด + * @return ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ + */ + fun getErrorMessage(candidate: T): String = "๊ฐ์ฒด๊ฐ€ ๋ช…์„ธ '${getName()}'๋ฅผ ๋งŒ์กฑํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค." +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/SpecificationResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/SpecificationResult.kt new file mode 100644 index 00000000..48689b4e --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/SpecificationResult.kt @@ -0,0 +1,21 @@ +package hs.kr.entrydsm.global.annotation.specification + +/** + * ๋ช…์„ธ ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * Specification ํŒจํ„ด์—์„œ ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๊ตฌ์กฐํ™”ํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * ์„ฑ๊ณต ์—ฌ๋ถ€, ๋ฉ”์‹œ์ง€, ์‚ฌ์šฉ๋œ ๋ช…์„ธ ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๊ฒ€์ฆ๋œ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @param success ๊ฒ€์ฆ ์„ฑ๊ณต ์—ฌ๋ถ€ + * @param message ๊ฒ€์ฆ ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€ + * @param specification ๊ฒ€์ฆ์— ์‚ฌ์šฉ๋œ ๋ช…์„ธ + * + * @author kangeunchan + * @since 2025.07.15 + */ +data class SpecificationResult( + val success: Boolean, + val message: String = "", + val specification: SpecificationContract +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/provider/CombineOperator.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/provider/CombineOperator.kt new file mode 100644 index 00000000..694266e4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/provider/CombineOperator.kt @@ -0,0 +1,11 @@ +package hs.kr.entrydsm.global.annotation.specification.provider + +/** + * ๋ช…์„ธ ๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + */ +enum class CombineOperator { + /** AND ์กฐ๊ฑด */ + AND, + /** OR ์กฐ๊ฑด */ + OR +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/provider/SpecificationProvider.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/provider/SpecificationProvider.kt new file mode 100644 index 00000000..a77b4bd4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/provider/SpecificationProvider.kt @@ -0,0 +1,320 @@ +package hs.kr.entrydsm.global.annotation.specification.provider + +import hs.kr.entrydsm.global.annotation.specification.Specification +import hs.kr.entrydsm.global.annotation.specification.SpecificationContract +import hs.kr.entrydsm.global.annotation.specification.SpecificationResult +import hs.kr.entrydsm.global.annotation.specification.type.Priority + +/** + * DDD์˜ Specification ํŒจํ„ด ๊ด€๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” Provider ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™(Specification)์˜ ๋“ฑ๋ก, ๊ฒ€์ฆ, ๊ฒ€์ƒ‰์„ ํ†ตํ•ด ๋„๋ฉ”์ธ ๋กœ์ง์„ + * ๋ช…์‹œ์ ์ด๊ณ  ์ฒด๊ณ„์ ์œผ๋กœ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋„๋ก ์ง€์›ํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.15 + */ +object SpecificationProvider { + + private val specificationRegistry = mutableMapOf>>() + private val specificationInstances = mutableMapOf, SpecificationContract<*>>() + private val domainRegistry = mutableMapOf>>() + private val priorityRegistry = mutableMapOf>>() + private val specificationCache = mutableMapOf>() + + /** + * ํƒ€์ž… ์•ˆ์ „์„ฑ์„ ์œ„ํ•œ ์ธ๋ผ์ธ ํ•จ์ˆ˜๋กœ ๋ช…์„ธ๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๋“ฑ๋กํ•  ๋ช…์„ธ ํด๋ž˜์Šค ํƒ€์ž… + */ + inline fun > registerSpecification() { + registerSpecification(T::class.java) + } + + /** + * ๋ช…์„ธ ํด๋ž˜์Šค๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + * + * @param specificationClass ๋“ฑ๋กํ•  ๋ช…์„ธ ํด๋ž˜์Šค + * @param T ๋ช…์„ธ ํด๋ž˜์Šค ํƒ€์ž… + * @throws IllegalArgumentException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun > registerSpecification(specificationClass: Class) { + validateSpecification(specificationClass) + + val annotation = getRuleAnnotation(specificationClass) + val name = annotation.name + val domain = annotation.domain + val priority = annotation.priority + + specificationRegistry.getOrPut(name) { mutableSetOf() }.add(specificationClass) + domainRegistry.getOrPut(domain) { mutableSetOf() }.add(specificationClass) + priorityRegistry.getOrPut(priority) { mutableSetOf() }.add(specificationClass) + } + + /** + * ํŠน์ • ๋ช…์„ธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ์ฒด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์‚ฌ์šฉํ•  ๋ช…์„ธ์˜ ์ด๋ฆ„ + * @param candidate ๊ฒ€์ฆํ•  ๊ฐ์ฒด + * @param T ๊ฒ€์ฆ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + * @throws IllegalArgumentException ๋ช…์„ธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun validateWithSpecification(name: String, candidate: T): SpecificationResult { + val specification = getSpecificationByName(name) + val success = specification.isSatisfiedBy(candidate) + val message = if (success) "๊ฒ€์ฆ ์„ฑ๊ณต" else specification.getErrorMessage(candidate) + + return SpecificationResult( + success = success, + message = message, + specification = specification + ) + } + + /** + * ๋„๋ฉ”์ธ์— ์†ํ•œ ์—ฌ๋Ÿฌ ๋ช…์„ธ๋“ค์„ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ์ฒด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param domain ๋Œ€์ƒ ๋„๋ฉ”์ธ ์ด๋ฆ„ + * @param candidate ๊ฒ€์ฆํ•  ๊ฐ์ฒด + * @param priority ํŠน์ • ์šฐ์„ ์ˆœ์œ„์˜ ๋ช…์„ธ๋งŒ ์‚ฌ์šฉํ•  ๊ฒฝ์šฐ ์ง€์ • + * @param T ๊ฒ€์ฆ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @return ๊ฐ ๋ช…์„ธ์— ๋Œ€ํ•œ ๊ฒ€์ฆ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ (์šฐ์„ ์ˆœ์œ„ ๋‚ด๋ฆผ์ฐจ์ˆœ) + */ + fun validateWithSpecifications( + domain: String, + candidate: T, + priority: Priority? = null + ): List> { + val specifications = getSpecificationsByDomain(domain) + .let { specs -> + if (priority != null) { + specs.filter { it.getPriority() == priority } + } else { + specs + } + } + .sortedByDescending { it.getPriority() } + + return specifications.map { specification -> + val success = specification.isSatisfiedBy(candidate) + val message = if (success) "๊ฒ€์ฆ ์„ฑ๊ณต" else specification.getErrorMessage(candidate) + + SpecificationResult( + success = success, + message = message, + specification = specification + ) + } + } + + /** + * ๋„๋ฉ”์ธ์˜ ๋ชจ๋“  ๋ช…์„ธ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฐ์ฒด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param domain ๋Œ€์ƒ ๋„๋ฉ”์ธ ์ด๋ฆ„ + * @param candidate ๊ฒ€์ฆํ•  ๊ฐ์ฒด + * @param failFast ์ฒซ ๋ฒˆ์งธ ์‹คํŒจ ์‹œ ์ฆ‰์‹œ false ๋ฐ˜ํ™˜ ์—ฌ๋ถ€ + * @param T ๊ฒ€์ฆ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @return ๋ชจ๋“  ๋ช…์„ธ๋ฅผ ๋งŒ์กฑํ•˜๋ฉด true, ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจํ•˜๋ฉด false + */ + fun validateWithAllSpecifications( + domain: String, + candidate: T, + failFast: Boolean = false + ): Boolean { + val specifications = getSpecificationsByDomain(domain) + .sortedByDescending { it.getPriority() } + + for (specification in specifications) { + if (!specification.isSatisfiedBy(candidate)) { + if (failFast) return false + } + } + + return true + } + + /** + * ์—ฌ๋Ÿฌ ๋ช…์„ธ๋ฅผ ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž๋กœ ๊ฒฐํ•ฉํ•˜์—ฌ ์ƒˆ๋กœ์šด ๋ณตํ•ฉ ๋ช…์„ธ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param names ๊ฒฐํ•ฉํ•  ๋ช…์„ธ๋“ค์˜ ์ด๋ฆ„ ๋ฆฌ์ŠคํŠธ + * @param operator ๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž (AND ๋˜๋Š” OR) + * @param T ๊ฒ€์ฆ ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @return ๊ฒฐํ•ฉ๋œ ๋ณตํ•ฉ ๋ช…์„ธ + * @throws IllegalArgumentException ๊ฒฐํ•ฉํ•  ๋ช…์„ธ๊ฐ€ ์—†๊ฑฐ๋‚˜ ๋ช…์„ธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ๊ฒฝ์šฐ + */ + fun combineSpecifications( + names: List, + operator: CombineOperator = CombineOperator.AND + ): SpecificationContract { + if (names.isEmpty()) { + throw IllegalArgumentException("๊ฒฐํ•ฉํ•  ๋ช…์„ธ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.") + } + + val specifications = names.map { getSpecificationByName(it) } + + return when (operator) { + CombineOperator.AND -> specifications.reduce { acc, spec -> acc.and(spec) } + CombineOperator.OR -> specifications.reduce { acc, spec -> acc.or(spec) } + } + } + + /** + * ์ด๋ฆ„์„ ํ†ตํ•ด ๋ช…์„ธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param name ์กฐํšŒํ•  ๋ช…์„ธ์˜ ์ด๋ฆ„ + * @param T ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @return ๋ช…์„ธ ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException ๋ช…์„ธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๊ฑฐ๋‚˜ ์ค‘๋ณต ๊ตฌํ˜„์ฒด๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ + */ + @Suppress("UNCHECKED_CAST") + fun getSpecificationByName(name: String): SpecificationContract { + return specificationCache.getOrPut(name) { + val specificationClasses = specificationRegistry[name] + ?: throw IllegalArgumentException("๋ช…์„ธ '$name'์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.") + + if (specificationClasses.size > 1) { + throw IllegalArgumentException("๋ช…์„ธ '$name'์— ๋Œ€ํ•ด ์—ฌ๋Ÿฌ ๊ตฌํ˜„์ฒด๊ฐ€ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค: ${specificationClasses.map { it.simpleName }}") + } + + getSpecificationInstance(specificationClasses.first()) + } as SpecificationContract + } + + /** + * ๋„๋ฉ”์ธ์— ์†ํ•œ ๋ชจ๋“  ๋ช…์„ธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param domain ๋Œ€์ƒ ๋„๋ฉ”์ธ ์ด๋ฆ„ + * @param T ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @return ๋„๋ฉ”์ธ์— ์†ํ•œ ๋ช…์„ธ ๋ฆฌ์ŠคํŠธ + */ + @Suppress("UNCHECKED_CAST") + fun getSpecificationsByDomain(domain: String): List> { + return domainRegistry[domain]?.map { getSpecificationInstance(it) as SpecificationContract } ?: emptyList() + } + + /** + * ํŠน์ • ์šฐ์„ ์ˆœ์œ„์— ํ•ด๋‹นํ•˜๋Š” ๋ชจ๋“  ๋ช…์„ธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param priority ๋Œ€์ƒ ์šฐ์„ ์ˆœ์œ„ + * @param T ๋Œ€์ƒ ๊ฐ์ฒด์˜ ํƒ€์ž… + * @return ํ•ด๋‹น ์šฐ์„ ์ˆœ์œ„์— ์†ํ•œ ๋ช…์„ธ ๋ฆฌ์ŠคํŠธ + */ + @Suppress("UNCHECKED_CAST") + fun getSpecificationsByPriority(priority: Priority): List> { + return priorityRegistry[priority]?.map { getSpecificationInstance(it) as SpecificationContract } ?: emptyList() + } + + /** + * ๋“ฑ๋ก๋œ ๋ชจ๋“  ๋ช…์„ธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋“ฑ๋ก๋œ ๋ชจ๋“  ๋ช…์„ธ ๋ฆฌ์ŠคํŠธ + */ + fun getAllSpecifications(): List> { + return specificationRegistry.values + .flatten() + .distinct() + .map { getSpecificationInstance(it) } + } + + /** + * ์ฃผ์–ด์ง„ ํด๋ž˜์Šค๊ฐ€ ์œ ํšจํ•œ ๋ช…์„ธ ํด๋ž˜์Šค์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param clazz ํ™•์ธํ•  ํด๋ž˜์Šค + * @return ์œ ํšจํ•œ ๋ช…์„ธ ํด๋ž˜์Šค์ด๋ฉด true, ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด false + */ + fun isSpecification(clazz: Class<*>): Boolean { + return hasRuleAnnotation(clazz) && SpecificationContract::class.java.isAssignableFrom(clazz) + } + + /** + * ๋ชจ๋“  ๋“ฑ๋ก๋œ ๋ช…์„ธ์™€ ์บ์‹œ๋ฅผ ์ง€์›๋‹ˆ๋‹ค. + */ + fun clearAll() { + specificationRegistry.clear() + specificationInstances.clear() + domainRegistry.clear() + priorityRegistry.clear() + specificationCache.clear() + } + + /** + * ๋ช…์„ธ ํด๋ž˜์Šค์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param specificationClass ๊ฒ€์ฆํ•  ๋ช…์„ธ ํด๋ž˜์Šค + * @throws IllegalArgumentException ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๊ฑฐ๋‚˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateSpecification(specificationClass: Class<*>) { + validateRuleAnnotation(specificationClass) + validateSpecificationContract(specificationClass) + } + + /** + * ๋ช…์„ธ ํด๋ž˜์Šค์— @Specification ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param specificationClass ๊ฒ€์ฆํ•  ๋ช…์„ธ ํด๋ž˜์Šค + * @throws IllegalArgumentException @Specification ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun validateRuleAnnotation(specificationClass: Class<*>) { + specificationClass.getAnnotation(Specification::class.java) + ?: throw IllegalArgumentException("ํด๋ž˜์Šค ${specificationClass.simpleName}์— @Specification ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค.") + } + + /** + * ๋ช…์„ธ ํด๋ž˜์Šค๊ฐ€ SpecificationContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param specificationClass ๊ฒ€์ฆํ•  ๋ช…์„ธ ํด๋ž˜์Šค + * @throws IllegalArgumentException SpecificationContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ + */ + fun validateSpecificationContract(specificationClass: Class<*>) { + if (!SpecificationContract::class.java.isAssignableFrom(specificationClass)) { + throw IllegalArgumentException("ํด๋ž˜์Šค ${specificationClass.simpleName}๋Š” SpecificationContract ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๊ตฌํ˜„ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.") + } + } + + /** + * ๋ช…์„ธ ํด๋ž˜์Šค์˜ ์ธ์Šคํ„ด์Šค๋ฅผ ์กฐํšŒํ•˜๊ฑฐ๋‚˜ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param specificationClass ๋Œ€์ƒ ๋ช…์„ธ ํด๋ž˜์Šค + * @return ๋ช…์„ธ ์ธ์Šคํ„ด์Šค (์บ์‹œ๋จ) + */ + fun getSpecificationInstance(specificationClass: Class<*>): SpecificationContract<*> { + return specificationInstances.getOrPut(specificationClass) { + createSpecificationInstance(specificationClass) + } + } + + /** + * ๋ช…์„ธ ํด๋ž˜์Šค์˜ ์ƒˆ๋กœ์šด ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param specificationClass ์ธ์Šคํ„ด์Šค๋ฅผ ์ƒ์„ฑํ•  ๋ช…์„ธ ํด๋ž˜์Šค + * @return ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ๋ช…์„ธ ์ธ์Šคํ„ด์Šค + * @throws RuntimeException ์ธ์Šคํ„ด์Šค ์ƒ์„ฑ์— ์‹คํŒจํ•œ ๊ฒฝ์šฐ + */ + @Suppress("UNCHECKED_CAST") + fun createSpecificationInstance(specificationClass: Class<*>): SpecificationContract<*> { + return specificationClass.getDeclaredConstructor().newInstance() as SpecificationContract<*> + } + + /** + * ๋ช…์„ธ ํด๋ž˜์Šค์—์„œ @Specification ์–ด๋…ธํ…Œ์ด์…˜์„ ๊ฐ€์ ธ์˜ต๋‹ˆ๋‹ค. + * + * @param specificationClass ๋Œ€์ƒ ๋ช…์„ธ ํด๋ž˜์Šค + * @return @Specification ์–ด๋…ธํ…Œ์ด์…˜ ์ธ์Šคํ„ด์Šค + * @throws IllegalArgumentException @Specification ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†๋Š” ๊ฒฝ์šฐ + */ + fun getRuleAnnotation(specificationClass: Class<*>): Specification { + return specificationClass.getAnnotation(Specification::class.java) + ?: throw IllegalArgumentException("ํด๋ž˜์Šค ${specificationClass.simpleName}์— @Specification ์–ด๋…ธํ…Œ์ด์…˜์ด ์—†์Šต๋‹ˆ๋‹ค.") + } + + /** + * ํด๋ž˜์Šค์— @Specification ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param clazz ํ™•์ธํ•  ํด๋ž˜์Šค + * @return @Specification ์–ด๋…ธํ…Œ์ด์…˜์ด ์žˆ์œผ๋ฉด true, ์—†์œผ๋ฉด false + */ + fun hasRuleAnnotation(clazz: Class<*>): Boolean { + return clazz.getAnnotation(Specification::class.java) != null + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/type/Priority.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/type/Priority.kt new file mode 100644 index 00000000..d085bf68 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/annotation/specification/type/Priority.kt @@ -0,0 +1,24 @@ +package hs.kr.entrydsm.global.annotation.specification.type + +/** + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™(Specification)์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + * + * ์—ฌ๋Ÿฌ ๊ทœ์น™์ด ์ ์šฉ๋  ๋•Œ ์‹คํ–‰ ์ˆœ์„œ๋‚˜ ์ค‘์š”๋„๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. + * ๋†’์€ ์šฐ์„ ์ˆœ์œ„์˜ ๊ทœ์น™์ด ๋จผ์ € ์ฒ˜๋ฆฌ๋˜๊ฑฐ๋‚˜ ๋” ์—„๊ฒฉํ•˜๊ฒŒ ์ ์šฉ๋  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.08 + */ +enum class Priority { + /** ๋‚ฎ์€ ์šฐ์„ ์ˆœ์œ„ - ์„ ํƒ์ ์ด๊ฑฐ๋‚˜ ๋ถ€์ฐจ์ ์ธ ๊ทœ์น™ */ + LOW, + + /** ์ผ๋ฐ˜ ์šฐ์„ ์ˆœ์œ„ - ํ‘œ์ค€์ ์ธ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ */ + NORMAL, + + /** ๋†’์€ ์šฐ์„ ์ˆœ์œ„ - ํ•„์ˆ˜์ ์ด๊ฑฐ๋‚˜ ์ค‘์š”ํ•œ ๊ทœ์น™ */ + HIGH, + + /** ์น˜๋ช…์  ์šฐ์„ ์ˆœ์œ„ - ์‹œ์Šคํ…œ์˜ ์•ˆ์ „์„ฑ๊ณผ ์ง๊ฒฐ๋˜๋Š” ๊ทœ์น™ */ + CRITICAL +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/ASTConfiguration.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/ASTConfiguration.kt new file mode 100644 index 00000000..1d1b0818 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/ASTConfiguration.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.global.configuration + +/** + * AST์˜ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.07 + */ +data class ASTConfiguration( + val maxDepth: Int = 1000, + val maxNodes: Int = 100000, + val enableOptimization: Boolean = true, + val cachingEnabled: Boolean = true, + val maxCacheSize: Int = 1000, + val enableValidation: Boolean = true, + val compressionEnabled: Boolean = false +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/CalculatorConfiguration.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/CalculatorConfiguration.kt new file mode 100644 index 00000000..af64aa21 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/CalculatorConfiguration.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.global.configuration + +/** + * ๊ณ„์‚ฐ๊ธฐ์˜ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.07 + */ +data class CalculatorConfiguration( + val maxFormulaLength: Int = 5000, + val maxVariables: Int = 100, + val defaultTimeoutMs: Long = 30000, + val maxRetries: Int = 3, + val concurrency: Int = 10, + val cachingEnabled: Boolean = true, + val maxCacheSize: Int = 1000, + val enableParallelProcessing: Boolean = true +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/EvaluatorConfiguration.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/EvaluatorConfiguration.kt new file mode 100644 index 00000000..9a50d76e --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/EvaluatorConfiguration.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.global.configuration + +/** + * ํ‰๊ฐ€๊ธฐ์˜ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.07 + */ +data class EvaluatorConfiguration( + val maxEvaluationDepth: Int = 1000, + val defaultTimeoutMs: Long = 10000, + val enableTypeChecking: Boolean = true, + val strictMathMode: Boolean = false, + val cachingEnabled: Boolean = true, + val maxCacheSize: Int = 1000, + val precisionDigits: Int = 15 +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/ExpresserConfiguration.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/ExpresserConfiguration.kt new file mode 100644 index 00000000..f123af49 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/ExpresserConfiguration.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.global.configuration + +/** + * ํ‘œํ˜„์‹ ์ฒ˜๋ฆฌ๊ธฐ์˜ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.07 + */ +data class ExpresserConfiguration( + val defaultTimeoutMs: Long = 30000, + val maxRetries: Int = 3, + val cachingEnabled: Boolean = true, + val maxCacheSize: Int = 1000, + val enableQualityCheck: Boolean = true, + val enableSecurityFilter: Boolean = true, + val supportedFormats: Set = setOf("mathematical", "latex", "mathml", "html", "json", "xml") +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/LexerConfiguration.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/LexerConfiguration.kt new file mode 100644 index 00000000..8b4551c4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/LexerConfiguration.kt @@ -0,0 +1,16 @@ +package hs.kr.entrydsm.global.configuration + +/** + * ๋ ‰์„œ์˜ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.07 + */ +data class LexerConfiguration( + val maxInputLength: Int = 100000, + val maxTokens: Int = 50000, + val bufferSize: Int = 8192, + val enableValidation: Boolean = true, + val strictMode: Boolean = false, + val debugMode: Boolean = false +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/ParserConfiguration.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/ParserConfiguration.kt new file mode 100644 index 00000000..a463ecb4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/ParserConfiguration.kt @@ -0,0 +1,33 @@ +package hs.kr.entrydsm.global.configuration + +/** + * ํŒŒ์„œ์˜ ์„ค์ •์„ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ํŒŒ์„œ ๋™์ž‘์— ํ•„์š”ํ•œ ๋ชจ๋“  ์„ค์ •๊ฐ’๋“ค์„ ์ค‘์•™์ง‘์ค‘์‹์œผ๋กœ ๊ด€๋ฆฌํ•˜์—ฌ + * ์„ค์ • ์ƒํƒœ ์ผ๊ด€์„ฑ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. Infrastructure ๊ณ„์ธต์—์„œ + * ConfigurationProvider๋ฅผ ํ†ตํ•ด ๋Ÿฐํƒ€์ž„์— ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @property debugMode ๋””๋ฒ„๊ทธ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @property errorRecoveryMode ์˜ค๋ฅ˜ ๋ณต๊ตฌ ๋ชจ๋“œ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @property maxParsingDepth ์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด + * @property maxParsingSteps ์ตœ๋Œ€ ํŒŒ์‹ฑ ๋‹จ๊ณ„ ์ˆ˜ + * @property maxStackDepth ์ตœ๋Œ€ ์Šคํƒ ๊นŠ์ด + * @property maxTokenCount ์ตœ๋Œ€ ํ† ํฐ ์ˆ˜ + * @property enableOptimizations ์ตœ์ ํ™” ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @property cachingEnabled ์บ์‹ฑ ํ™œ์„ฑํ™” ์—ฌ๋ถ€ + * @property streamingBatchSize ์ŠคํŠธ๋ฆฌ๋ฐ ๋ฐฐ์น˜ ํฌ๊ธฐ + * + * @author kangeunchan + * @since 2025.08.07 + */ +data class ParserConfiguration( + val debugMode: Boolean = false, + val errorRecoveryMode: Boolean = true, + val maxParsingDepth: Int = 10000, + val maxParsingSteps: Int = 100000, + val maxStackDepth: Int = 10000, + val maxTokenCount: Int = 50000, + val enableOptimizations: Boolean = true, + val cachingEnabled: Boolean = true, + val streamingBatchSize: Int = 100 +) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/interfaces/ConfigurationChangeListener.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/interfaces/ConfigurationChangeListener.kt new file mode 100644 index 00000000..902caa1a --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/interfaces/ConfigurationChangeListener.kt @@ -0,0 +1,41 @@ +package hs.kr.entrydsm.global.configuration.interfaces + +/** + * ์„ค์ • ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๋Š” ๋ฆฌ์Šค๋„ˆ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋“ค์ด ์„ค์ • ๋ณ€๊ฒฝ์— ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ˜์‘ํ•  ์ˆ˜ ์žˆ๋„๋ก ํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.07 + */ +interface ConfigurationChangeListener { + + /** + * ์„ค์ •์ด ๋ณ€๊ฒฝ๋˜์—ˆ์„ ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. + * + * @param configType ๋ณ€๊ฒฝ๋œ ์„ค์ • ํƒ€์ž… + * @param oldConfig ์ด์ „ ์„ค์ • + * @param newConfig ์ƒˆ๋กœ์šด ์„ค์ • + */ + fun onConfigurationChanged( + configType: String, + oldConfig: Any, + newConfig: Any + ) + + /** + * ์„ค์ • ๋ณ€๊ฒฝ์ด ์‹คํŒจํ–ˆ์„ ๋•Œ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. + * + * @param configType ๋ณ€๊ฒฝ ์‹œ๋„ํ•œ ์„ค์ • ํƒ€์ž… + * @param error ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜ + */ + fun onConfigurationChangeFailed( + configType: String, + error: Exception + ) + + /** + * ๋ฆฌ์Šค๋„ˆ๊ฐ€ ๊ด€์‹ฌ์žˆ๋Š” ์„ค์ • ํƒ€์ž…๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getInterestedConfigTypes(): Set +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/interfaces/ConfigurationProvider.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/interfaces/ConfigurationProvider.kt new file mode 100644 index 00000000..e00e90a7 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/configuration/interfaces/ConfigurationProvider.kt @@ -0,0 +1,85 @@ +package hs.kr.entrydsm.global.configuration.interfaces + +import hs.kr.entrydsm.global.configuration.* + +/** + * ์‹œ์Šคํ…œ ์„ค์ •์„ ์ œ๊ณตํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Infrastructure ๊ณ„์ธต์—์„œ ๊ตฌํ˜„ํ•˜์—ฌ ๋Ÿฐํƒ€์ž„์— ์„ค์ •๊ฐ’์„ ์ˆ˜์ •ํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ฉ๋‹ˆ๋‹ค. + * ๋‹ค์–‘ํ•œ ์†Œ์Šค(ํŒŒ์ผ, ํ™˜๊ฒฝ๋ณ€์ˆ˜, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ๋“ฑ)์—์„œ ์„ค์ •์„ ์ฝ์–ด์˜ฌ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.07 + */ +interface ConfigurationProvider { + + /** + * ํŒŒ์„œ ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getParserConfiguration(): ParserConfiguration + + /** + * ๊ณ„์‚ฐ๊ธฐ ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getCalculatorConfiguration(): CalculatorConfiguration + + /** + * ๋ ‰์„œ ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getLexerConfiguration(): LexerConfiguration + + /** + * AST ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getASTConfiguration(): ASTConfiguration + + /** + * ํ‘œํ˜„์‹ ์ฒ˜๋ฆฌ ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getExpresserConfiguration(): ExpresserConfiguration + + /** + * ํ‰๊ฐ€๊ธฐ ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getEvaluatorConfiguration(): EvaluatorConfiguration + + /** + * ํŠน์ • ์„ค์ •์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. + */ + fun updateParserConfiguration(configuration: ParserConfiguration) + fun updateCalculatorConfiguration(configuration: CalculatorConfiguration) + fun updateLexerConfiguration(configuration: LexerConfiguration) + fun updateASTConfiguration(configuration: ASTConfiguration) + fun updateExpresserConfiguration(configuration: ExpresserConfiguration) + fun updateEvaluatorConfiguration(configuration: EvaluatorConfiguration) + + /** + * ๋ชจ๋“  ์„ค์ •์„ ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + */ + fun resetToDefaults() + + /** + * ์„ค์ • ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + */ + fun saveConfiguration() + + /** + * ์„ค์ • ๋ณ€๊ฒฝ์„ ๊ฐ์ง€ํ•˜๋Š” ๋ฆฌ์Šค๋„ˆ๋ฅผ ๋“ฑ๋กํ•ฉ๋‹ˆ๋‹ค. + */ + fun addConfigurationChangeListener(listener: ConfigurationChangeListener) + + /** + * ์„ค์ • ๋ณ€๊ฒฝ ๋ฆฌ์Šค๋„ˆ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + */ + fun removeConfigurationChangeListener(listener: ConfigurationChangeListener) + + /** + * ํ˜„์žฌ ์„ค์ • ์ƒํƒœ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ + fun validateConfiguration(): Map> + + /** + * ์„ค์ •์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getConfigurationMetadata(): Map +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/ErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/ErrorCodes.kt new file mode 100644 index 00000000..af81f632 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/ErrorCodes.kt @@ -0,0 +1,155 @@ +package hs.kr.entrydsm.global.constants + +import hs.kr.entrydsm.global.constants.error.* + +/** + * ์‹œ์Šคํ…œ ์ „์—ญ์—์„œ ์‚ฌ์šฉ๋˜๋Š” ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ๊ด€๋ฆฌํ•˜๋Š” ํ†ตํ•ฉ ํŒฉํ† ๋ฆฌ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋ชจ๋“  ์—๋Ÿฌ ์ฝ”๋“œ๋Š” ๊ฐœ๋ณ„ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ๋˜์–ด ๋‹จ์ผ ์ฑ…์ž„ ์›์น™์„ ์ค€์ˆ˜ํ•˜๋ฉฐ, + * ์ด ํด๋ž˜์Šค๋Š” ๊ฐ ๋„๋ฉ”์ธ๋ณ„ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์˜ ์ ‘๊ทผ์ ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object ErrorCodes { + + // ๊ณตํ†ต ์—๋Ÿฌ ์ฝ”๋“œ (0000-0999) + val Common = CommonErrorCodes + + // ๋ ‰์„œ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ (1000-1999) + val Lexer = LexerErrorCodes + + // ํŒŒ์„œ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ (2000-2999) + val Parser = ParserErrorCodes + + // AST ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ (3000-3999) + val AST = ASTErrorCodes + + // ํ‰๊ฐ€๊ธฐ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ (4000-4999) + val Evaluator = EvaluatorErrorCodes + + // ๊ณ„์‚ฐ๊ธฐ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ (5000-5999) + val Calculator = CalculatorErrorCodes + + // ํ‘œํ˜„๊ธฐ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ (6000-6999) + val Expresser = ExpresserErrorCodes + + // ํŒฉํ† ๋ฆฌ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ (7000-7999) + val Factory = FactoryErrorCodes + + // ๋ช…์„ธ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ (8000-8999) + val Specification = SpecificationErrorCodes + + // ์ •์ฑ… ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ (9000-9999) + val Policy = PolicyErrorCodes + + /** + * ์—๋Ÿฌ ์ฝ”๋“œ์˜ ๋„๋ฉ”์ธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getDomain(errorCode: String): String { + return when { + errorCode.startsWith("C0") -> "Common" + errorCode.startsWith("L") -> "Lexer" + errorCode.startsWith("P2") -> "Parser" + errorCode.startsWith("A") -> "AST" + errorCode.startsWith("E") -> "Evaluator" + errorCode.startsWith("C5") -> "Calculator" + errorCode.startsWith("X") -> "Expresser" + errorCode.startsWith("F") -> "Factory" + errorCode.startsWith("S") -> "Specification" + errorCode.startsWith("P9") -> "Policy" + else -> "Unknown" + } + } + + /** + * ์—๋Ÿฌ ์ฝ”๋“œ์˜ ์‹ฌ๊ฐ๋„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getSeverity(errorCode: String): String { + return when { + errorCode.endsWith("01") -> "CRITICAL" + errorCode.endsWith("02") -> "ERROR" + errorCode.endsWith("03") -> "ERROR" + errorCode.endsWith("04") -> "WARNING" + errorCode.endsWith("05") -> "WARNING" + errorCode.endsWith("06") -> "INFO" + errorCode.endsWith("07") -> "INFO" + errorCode.endsWith("08") -> "DEBUG" + errorCode.endsWith("09") -> "DEBUG" + errorCode.endsWith("10") -> "TRACE" + else -> "ERROR" + } + } + + /** + * ๋ชจ๋“  ์—๋Ÿฌ ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun getAllErrorCodes(): Set = setOf( + // Common + Common.UNKNOWN_ERROR.code, Common.INVALID_ARGUMENT.code, Common.NULL_POINTER.code, + Common.ILLEGAL_STATE.code, Common.TIMEOUT.code, Common.PERMISSION_DENIED.code, + Common.RESOURCE_NOT_FOUND.code, Common.RESOURCE_ALREADY_EXISTS.code, + Common.CONFIGURATION_ERROR.code, Common.VALIDATION_FAILED.code, + + // Lexer + Lexer.TOKENIZATION_FAILED.code, Lexer.INVALID_CHARACTER.code, Lexer.UNEXPECTED_TOKEN.code, + Lexer.INVALID_NUMBER_FORMAT.code, Lexer.INVALID_STRING_LITERAL.code, + Lexer.UNCLOSED_STRING.code, Lexer.INVALID_IDENTIFIER.code, Lexer.TOKEN_POSITION_ERROR.code, + Lexer.LEXER_STATE_ERROR.code, Lexer.CHARACTER_ENCODING_ERROR.code, + + // Parser + Parser.PARSING_FAILED.code, Parser.SYNTAX_ERROR.code, Parser.UNEXPECTED_EOF.code, + Parser.GRAMMAR_VIOLATION.code, Parser.LR_CONFLICT.code, Parser.SHIFT_REDUCE_CONFLICT.code, + Parser.REDUCE_REDUCE_CONFLICT.code, Parser.INVALID_PRODUCTION.code, + Parser.PARSER_STATE_ERROR.code, Parser.AST_CONSTRUCTION_FAILED.code, + + // AST + AST.NODE_CREATION_FAILED.code, AST.INVALID_NODE_TYPE.code, AST.NODE_VALIDATION_FAILED.code, + AST.TREE_STRUCTURE_ERROR.code, AST.VISITOR_PATTERN_ERROR.code, AST.NODE_TRAVERSAL_ERROR.code, + AST.TREE_OPTIMIZATION_FAILED.code, AST.CIRCULAR_REFERENCE.code, AST.MAX_DEPTH_EXCEEDED.code, + AST.INVALID_NODE_RELATIONSHIP.code, + + // Evaluator + Evaluator.EVALUATION_FAILED.code, Evaluator.UNDEFINED_VARIABLE.code, Evaluator.TYPE_MISMATCH.code, + Evaluator.DIVISION_BY_ZERO.code, Evaluator.FUNCTION_NOT_FOUND.code, + Evaluator.INVALID_FUNCTION_ARGUMENTS.code, Evaluator.ARITHMETIC_OVERFLOW.code, + Evaluator.INVALID_OPERATION.code, Evaluator.CONTEXT_ERROR.code, Evaluator.SECURITY_VIOLATION.code, + Evaluator.PERFORMANCE_LIMIT_EXCEEDED.code, Evaluator.UNSUPPORTED_TYPE.code, + + // Calculator + Calculator.CALCULATION_FAILED.code, Calculator.FORMULA_TOO_LONG.code, + Calculator.TOO_MANY_VARIABLES.code, Calculator.INVALID_FORMULA.code, + Calculator.CALCULATION_TIMEOUT.code, Calculator.MEMORY_LIMIT_EXCEEDED.code, + Calculator.STEP_LIMIT_EXCEEDED.code, Calculator.RECURSIVE_CALCULATION.code, + Calculator.INVALID_RESULT.code, Calculator.CALCULATION_INTERRUPTED.code, + + // Expresser + Expresser.FORMATTING_FAILED, Expresser.INVALID_FORMAT_STYLE, + Expresser.EXPRESSION_TOO_COMPLEX, Expresser.FORMATTING_TIMEOUT, + Expresser.UNSUPPORTED_NODE_TYPE, Expresser.FORMAT_VALIDATION_FAILED, + Expresser.STYLE_CONFIGURATION_ERROR, Expresser.OUTPUT_BUFFER_OVERFLOW, + Expresser.ENCODING_ERROR, Expresser.FORMAT_TEMPLATE_ERROR, + + // Factory + Factory.CREATION_FAILED, Factory.INVALID_FACTORY_TYPE, + Factory.FACTORY_CONFIGURATION_ERROR, Factory.DEPENDENCY_INJECTION_FAILED, + Factory.FACTORY_CACHE_ERROR, Factory.CIRCULAR_DEPENDENCY, + Factory.FACTORY_STATE_ERROR, Factory.INVALID_FACTORY_CONTEXT, + Factory.FACTORY_INITIALIZATION_FAILED, Factory.FACTORY_TIMEOUT, + + // Specification + Specification.SPECIFICATION_FAILED, Specification.INVALID_SPECIFICATION, + Specification.SPECIFICATION_COMPOSITION_FAILED, + Specification.SPECIFICATION_EVALUATION_ERROR, + Specification.UNSUPPORTED_SPECIFICATION_TYPE, Specification.SPECIFICATION_TIMEOUT, + Specification.SPECIFICATION_DEPENDENCY_ERROR, Specification.SPECIFICATION_CACHE_ERROR, + Specification.COMPLEX_SPECIFICATION_LIMIT, Specification.SPECIFICATION_VALIDATION_FAILED, + + // Policy + Policy.POLICY_VIOLATION, Policy.INVALID_POLICY, Policy.POLICY_CONFLICT, + Policy.POLICY_EVALUATION_FAILED, Policy.UNSUPPORTED_POLICY_TYPE, + Policy.POLICY_TIMEOUT, Policy.POLICY_DEPENDENCY_ERROR, + Policy.POLICY_CONFIGURATION_ERROR, Policy.POLICY_ENFORCEMENT_FAILED, + Policy.POLICY_CHAIN_ERROR + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/NamingConventions.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/NamingConventions.kt new file mode 100644 index 00000000..8821f818 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/NamingConventions.kt @@ -0,0 +1,339 @@ +package hs.kr.entrydsm.global.constants + +/** + * ํ”„๋กœ์ ํŠธ ์ „๋ฐ˜์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋ช…๋ช… ๊ทœ์น™์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * POC ์ฝ”๋“œ์˜ ๋ช…๋ช… ๊ทœ์น™์„ ๊ธฐ๋ฐ˜์œผ๋กœ ์ผ๊ด€๋œ ๋„ค์ด๋ฐ ํŒจํ„ด์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * DDD ํŒจํ„ด๋ณ„๋กœ ๊ตฌ๋ถ„ํ•˜์—ฌ ๋ช…๋ช… ๊ทœ์น™์„ ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.28 + */ +object NamingConventions { + + /** + * DDD Aggregate ๋ช…๋ช… ๊ทœ์น™ + */ + object Aggregate { + const val SUFFIX = "" // Calculator, Parser, Lexer + const val PATTERN = "^[A-Z][a-zA-Z0-9]*$" + + val EXAMPLES = listOf( + "Calculator", "ExpressionEvaluator", "LRParser", + "ExpressionAST", "LexerAggregate" + ) + } + + /** + * DDD Entity ๋ช…๋ช… ๊ทœ์น™ + */ + object Entity { + const val SUFFIX = "" // Token, Production, ASTNode + const val PATTERN = "^[A-Z][a-zA-Z0-9]*$" + + val EXAMPLES = listOf( + "Token", "Production", "ASTNode", "MathFunction", + "CalculationSession", "EvaluationContext" + ) + } + + /** + * DDD Value Object ๋ช…๋ช… ๊ทœ์น™ + */ + object ValueObject { + const val SUFFIX = "" // TokenType, CalculationResult + const val PATTERN = "^[A-Z][a-zA-Z0-9]*$" + + val EXAMPLES = listOf( + "TokenType", "CalculationResult", "Grammar", "LRAction", + "VariableBinding", "EvaluationResult" + ) + } + + /** + * DDD Factory ๋ช…๋ช… ๊ทœ์น™ + */ + object Factory { + const val SUFFIX = "Factory" + const val PATTERN = "^[A-Z][a-zA-Z0-9]*Factory$" + + val EXAMPLES = listOf( + "EvaluatorFactory", "CalculatorFactory", "TokenFactory", + "ASTNodeFactory", "ParserFactory" + ) + } + + /** + * DDD Specification ๋ช…๋ช… ๊ทœ์น™ + */ + object Specification { + const val SUFFIX = "Spec" // POC ์ฝ”๋“œ ์Šคํƒ€์ผ ๋ฐ˜์˜ + const val PATTERN = "^[A-Z][a-zA-Z0-9]*Spec$" + + val EXAMPLES = listOf( + "ExpressionValiditySpec", "TypeCompatibilitySpec", + "CalculatorValiditySpec", "LRParsingValiditySpec" + ) + } + + /** + * DDD Policy ๋ช…๋ช… ๊ทœ์น™ + */ + object Policy { + const val SUFFIX = "Policy" + const val PATTERN = "^[A-Z][a-zA-Z0-9]*Policy$" + + val EXAMPLES = listOf( + "EvaluationPolicy", "TypeCoercionPolicy", + "CalculationPerformancePolicy", "TokenizationPolicy" + ) + } + + /** + * DDD Service ๋ช…๋ช… ๊ทœ์น™ + */ + object Service { + const val SUFFIX = "Service" + const val PATTERN = "^[A-Z][a-zA-Z0-9]*Service$" + + val EXAMPLES = listOf( + "CalculatorService", "MathFunctionService", "ParserService", + "ExpresserService", "ValidationService" + ) + } + + /** + * DDD Repository ๋ช…๋ช… ๊ทœ์น™ (๋ฏธ๋ž˜ ํ™•์žฅ์šฉ) + */ + object Repository { + const val SUFFIX = "Repository" + const val PATTERN = "^[A-Z][a-zA-Z0-9]*Repository$" + + val EXAMPLES = listOf( + "CalculationRepository", "SessionRepository" + ) + } + + /** + * Exception ๋ช…๋ช… ๊ทœ์น™ + */ + object Exception { + const val SUFFIX = "Exception" + const val PATTERN = "^[A-Z][a-zA-Z0-9]*Exception$" + + val EXAMPLES = listOf( + "EvaluatorException", "CalculatorException", "ParserException", + "LexerException", "ASTException", "ExpresserException" + ) + } + + /** + * Interface/Contract ๋ช…๋ช… ๊ทœ์น™ + */ + object Contract { + const val SUFFIX = "Contract" + const val PATTERN = "^[A-Z][a-zA-Z0-9]*Contract$" + + val EXAMPLES = listOf( + "EvaluatorContract", "CalculatorContract", "ParserContract", + "ASTVisitorContract", "ExpresserContract", "LexerContract" + ) + } + + /** + * Method ๋ช…๋ช… ๊ทœ์น™ (POC ์ฝ”๋“œ ๊ธฐ๋ฐ˜) + */ + object Method { + // POC ์ฝ”๋“œ์˜ ์ฃผ์š” ๋ฉ”์„œ๋“œ ํŒจํ„ด๋“ค + val CREATION_METHODS = listOf( + "create", "createDefault", "createWith", "createFrom", + "build", "generate", "construct" + ) + + val VALIDATION_METHODS = listOf( + "validate", "check", "verify", "ensure", "assert", + "isSatisfiedBy", "isValid", "meets" + ) + + val CALCULATION_METHODS = listOf( + "calculate", "evaluate", "compute", "process", + "execute", "apply", "perform" + ) + + val PARSING_METHODS = listOf( + "parse", "tokenize", "analyze", "recognize", + "scan", "lex", "transform" + ) + + val GETTER_PREFIX = "get" + val SETTER_PREFIX = "set" + val BOOLEAN_PREFIX = listOf("is", "has", "can", "should", "will") + } + + /** + * Constant ๋ช…๋ช… ๊ทœ์น™ + */ + object Constant { + const val PATTERN = "^[A-Z][A-Z0-9_]*$" + + val EXAMPLES = listOf( + "DEFAULT_MAX_FORMULA_LENGTH", "MAX_EXECUTION_TIME_MS", + "CACHE_TTL_SECONDS", "OPERATOR_PRECEDENCE" + ) + } + + /** + * Package ๋ช…๋ช… ๊ทœ์น™ + */ + object Package { + const val PATTERN = "^[a-z][a-z0-9.]*$" + + val DOMAIN_PACKAGES = listOf( + "hs.kr.entrydsm.domain.calculator", + "hs.kr.entrydsm.domain.evaluator", + "hs.kr.entrydsm.domain.parser", + "hs.kr.entrydsm.domain.lexer", + "hs.kr.entrydsm.domain.ast", + "hs.kr.entrydsm.domain.expresser" + ) + + val SUBDOMAIN_SUFFIXES = listOf( + "aggregates", "entities", "values", "factories", + "specifications", "policies", "services", "exceptions", + "interfaces" + ) + } + + /** + * ๋ช…๋ช… ๊ทœ์น™ ๊ฒ€์ฆ ๋ฉ”์„œ๋“œ๋“ค + */ + object Validation { + + fun isValidAggregateName(name: String): Boolean { + return name.matches(Regex(Aggregate.PATTERN)) && + !name.endsWith(Factory.SUFFIX) && + !name.endsWith(Service.SUFFIX) + } + + fun isValidFactoryName(name: String): Boolean { + return name.matches(Regex(Factory.PATTERN)) + } + + fun isValidSpecificationName(name: String): Boolean { + return name.matches(Regex(Specification.PATTERN)) + } + + fun isValidPolicyName(name: String): Boolean { + return name.matches(Regex(Policy.PATTERN)) + } + + fun isValidServiceName(name: String): Boolean { + return name.matches(Regex(Service.PATTERN)) + } + + fun isValidExceptionName(name: String): Boolean { + return name.matches(Regex(Exception.PATTERN)) + } + + fun isValidContractName(name: String): Boolean { + return name.matches(Regex(Contract.PATTERN)) + } + + fun isValidConstantName(name: String): Boolean { + return name.matches(Regex(Constant.PATTERN)) + } + + fun isValidPackageName(name: String): Boolean { + return name.matches(Regex(Package.PATTERN)) + } + + fun suggestCorrectName(incorrectName: String, type: String): String { + return when (type.lowercase()) { + "factory" -> { + if (!incorrectName.endsWith(Factory.SUFFIX)) { + "${incorrectName}${Factory.SUFFIX}" + } else incorrectName + } + "specification", "spec" -> { + if (!incorrectName.endsWith(Specification.SUFFIX)) { + "${incorrectName}${Specification.SUFFIX}" + } else incorrectName + } + "policy" -> { + if (!incorrectName.endsWith(Policy.SUFFIX)) { + "${incorrectName}${Policy.SUFFIX}" + } else incorrectName + } + "service" -> { + if (!incorrectName.endsWith(Service.SUFFIX)) { + "${incorrectName}${Service.SUFFIX}" + } else incorrectName + } + "exception" -> { + if (!incorrectName.endsWith(Exception.SUFFIX)) { + "${incorrectName}${Exception.SUFFIX}" + } else incorrectName + } + "contract" -> { + if (!incorrectName.endsWith(Contract.SUFFIX)) { + "${incorrectName}${Contract.SUFFIX}" + } else incorrectName + } + else -> incorrectName + } + } + } + + /** + * POC ์ฝ”๋“œ์—์„œ ์ถ”์ถœํ•œ ๋ช…๋ช… ํŒจํ„ด๋“ค + */ + object POCPatterns { + // POC ์ฝ”๋“œ์˜ ์ฃผ์š” ํด๋ž˜์Šค๋ช…๋“ค + val MAIN_CLASSES = listOf( + "ProductionCalculatorApplication", "CalculatorProperties", + "CalculatorController", "CalculatorService", "RealLRParser", + "Grammar", "LRParserTable", "CalculatorLexer", "ExpressionEvaluator" + ) + + // POC ์ฝ”๋“œ์˜ ์ฃผ์š” ๋ฉ”์„œ๋“œ๋ช…๋“ค + val MAIN_METHODS = listOf( + "calculate", "calculateMultiStep", "getParserInfo", + "parse", "lrParse", "tokenize", "evaluate", "accept", + "getAction", "getGoto", "buildStates", "buildTables" + ) + + // POC ์ฝ”๋“œ์˜ ์ฃผ์š” ์ƒ์ˆ˜๋ช…๋“ค + val MAIN_CONSTANTS = listOf( + "DEFAULT_MAX_DEPTH", "DEFAULT_MAX_NODES", "DEFAULT_MAX_VARIABLES", + "DEFAULT_MAX_EXECUTION_TIME_MS", "ALLOWED_FUNCTIONS", "ALLOWED_OPERATORS" + ) + } +} + +/** + * ๋ช…๋ช… ๊ทœ์น™ ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + */ +data class NamingValidationResult( + val isValid: Boolean, + val violations: List = emptyList(), + val suggestions: List = emptyList() +) + +/** + * ๋ช…๋ช… ๊ทœ์น™ ์œ„๋ฐ˜์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค + */ +data class NamingViolation( + val name: String, + val expectedPattern: String, + val actualPattern: String, + val violationType: ViolationType, + val suggestion: String +) + +/** + * ๋ช…๋ช… ๊ทœ์น™ ์œ„๋ฐ˜ ํƒ€์ž… + */ +enum class ViolationType { + SUFFIX_MISSING, SUFFIX_INCORRECT, PATTERN_MISMATCH, + CASE_INCORRECT, RESERVED_WORD, INCONSISTENT_STYLE +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/ASTErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/ASTErrorCodes.kt new file mode 100644 index 00000000..313ca439 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/ASTErrorCodes.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * AST ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object ASTErrorCodes { + val NODE_CREATION_FAILED = hs.kr.entrydsm.global.exception.ErrorCode.AST_BUILD_ERROR + val INVALID_NODE_TYPE = hs.kr.entrydsm.global.exception.ErrorCode.UNSUPPORTED_AST_TYPE + val NODE_VALIDATION_FAILED = hs.kr.entrydsm.global.exception.ErrorCode.AST_VALIDATION_FAILED + val TREE_STRUCTURE_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.INVALID_NODE_STRUCTURE + val VISITOR_PATTERN_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.AST_TRAVERSAL_ERROR + val NODE_TRAVERSAL_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.AST_TRAVERSAL_ERROR + val TREE_OPTIMIZATION_FAILED = hs.kr.entrydsm.global.exception.ErrorCode.AST_OPTIMIZATION_FAILED + val CIRCULAR_REFERENCE = hs.kr.entrydsm.global.exception.ErrorCode.INVALID_NODE_STRUCTURE + val MAX_DEPTH_EXCEEDED = hs.kr.entrydsm.global.exception.ErrorCode.AST_DEPTH_EXCEEDED + val INVALID_NODE_RELATIONSHIP = hs.kr.entrydsm.global.exception.ErrorCode.INVALID_NODE_STRUCTURE +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/CalculatorErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/CalculatorErrorCodes.kt new file mode 100644 index 00000000..321f05d4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/CalculatorErrorCodes.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * ๊ณ„์‚ฐ๊ธฐ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object CalculatorErrorCodes { + val CALCULATION_FAILED = hs.kr.entrydsm.global.exception.ErrorCode.EVALUATION_ERROR + val FORMULA_TOO_LONG = hs.kr.entrydsm.global.exception.ErrorCode.FORMULA_TOO_LONG + val TOO_MANY_VARIABLES = hs.kr.entrydsm.global.exception.ErrorCode.TOO_MANY_VARIABLES + val INVALID_FORMULA = hs.kr.entrydsm.global.exception.ErrorCode.EMPTY_FORMULA + val CALCULATION_TIMEOUT = hs.kr.entrydsm.global.exception.ErrorCode.INTERNAL_SERVER_ERROR + val MEMORY_LIMIT_EXCEEDED = hs.kr.entrydsm.global.exception.ErrorCode.INTERNAL_SERVER_ERROR + val STEP_LIMIT_EXCEEDED = hs.kr.entrydsm.global.exception.ErrorCode.TOO_MANY_STEPS + val RECURSIVE_CALCULATION = hs.kr.entrydsm.global.exception.ErrorCode.BUSINESS_RULE_VIOLATION + val INVALID_RESULT = hs.kr.entrydsm.global.exception.ErrorCode.EVALUATION_ERROR + val CALCULATION_INTERRUPTED = hs.kr.entrydsm.global.exception.ErrorCode.INTERNAL_SERVER_ERROR +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/CommonErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/CommonErrorCodes.kt new file mode 100644 index 00000000..8c1ec6a6 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/CommonErrorCodes.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * ๊ณตํ†ต ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object CommonErrorCodes { + val UNKNOWN_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.UNKNOWN_ERROR + val INVALID_ARGUMENT = hs.kr.entrydsm.global.exception.ErrorCode.VALIDATION_FAILED + val NULL_POINTER = hs.kr.entrydsm.global.exception.ErrorCode.INTERNAL_SERVER_ERROR + val ILLEGAL_STATE = hs.kr.entrydsm.global.exception.ErrorCode.BUSINESS_RULE_VIOLATION + val TIMEOUT = hs.kr.entrydsm.global.exception.ErrorCode.INTERNAL_SERVER_ERROR + val PERMISSION_DENIED = hs.kr.entrydsm.global.exception.ErrorCode.BUSINESS_RULE_VIOLATION + val RESOURCE_NOT_FOUND = hs.kr.entrydsm.global.exception.ErrorCode.VALIDATION_FAILED + val RESOURCE_ALREADY_EXISTS = hs.kr.entrydsm.global.exception.ErrorCode.BUSINESS_RULE_VIOLATION + val CONFIGURATION_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.INTERNAL_SERVER_ERROR + val VALIDATION_FAILED = hs.kr.entrydsm.global.exception.ErrorCode.VALIDATION_FAILED +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/EvaluatorErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/EvaluatorErrorCodes.kt new file mode 100644 index 00000000..2b787306 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/EvaluatorErrorCodes.kt @@ -0,0 +1,22 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * ํ‰๊ฐ€๊ธฐ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object EvaluatorErrorCodes { + val EVALUATION_FAILED = hs.kr.entrydsm.global.exception.ErrorCode.EVALUATION_ERROR + val UNDEFINED_VARIABLE = hs.kr.entrydsm.global.exception.ErrorCode.UNDEFINED_VARIABLE + val TYPE_MISMATCH = hs.kr.entrydsm.global.exception.ErrorCode.UNSUPPORTED_TYPE + val DIVISION_BY_ZERO = hs.kr.entrydsm.global.exception.ErrorCode.DIVISION_BY_ZERO + val FUNCTION_NOT_FOUND = hs.kr.entrydsm.global.exception.ErrorCode.UNSUPPORTED_FUNCTION + val INVALID_FUNCTION_ARGUMENTS = hs.kr.entrydsm.global.exception.ErrorCode.WRONG_ARGUMENT_COUNT + val ARITHMETIC_OVERFLOW = hs.kr.entrydsm.global.exception.ErrorCode.MATH_ERROR + val INVALID_OPERATION = hs.kr.entrydsm.global.exception.ErrorCode.UNSUPPORTED_OPERATOR + val CONTEXT_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.EVALUATION_ERROR + val SECURITY_VIOLATION = hs.kr.entrydsm.global.exception.ErrorCode.BUSINESS_RULE_VIOLATION + val PERFORMANCE_LIMIT_EXCEEDED = hs.kr.entrydsm.global.exception.ErrorCode.EVALUATION_ERROR + val UNSUPPORTED_TYPE = hs.kr.entrydsm.global.exception.ErrorCode.UNSUPPORTED_TYPE +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/ExpresserErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/ExpresserErrorCodes.kt new file mode 100644 index 00000000..b459303d --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/ExpresserErrorCodes.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * ํ‘œํ˜„๊ธฐ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object ExpresserErrorCodes { + const val FORMATTING_FAILED = "X6001" + const val INVALID_FORMAT_STYLE = "X6002" + const val EXPRESSION_TOO_COMPLEX = "X6003" + const val FORMATTING_TIMEOUT = "X6004" + const val UNSUPPORTED_NODE_TYPE = "X6005" + const val FORMAT_VALIDATION_FAILED = "X6006" + const val STYLE_CONFIGURATION_ERROR = "X6007" + const val OUTPUT_BUFFER_OVERFLOW = "X6008" + const val ENCODING_ERROR = "X6009" + const val FORMAT_TEMPLATE_ERROR = "X6010" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/FactoryErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/FactoryErrorCodes.kt new file mode 100644 index 00000000..948deb2e --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/FactoryErrorCodes.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * ํŒฉํ† ๋ฆฌ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object FactoryErrorCodes { + const val CREATION_FAILED = "F7001" + const val INVALID_FACTORY_TYPE = "F7002" + const val FACTORY_CONFIGURATION_ERROR = "F7003" + const val DEPENDENCY_INJECTION_FAILED = "F7004" + const val FACTORY_CACHE_ERROR = "F7005" + const val CIRCULAR_DEPENDENCY = "F7006" + const val FACTORY_STATE_ERROR = "F7007" + const val INVALID_FACTORY_CONTEXT = "F7008" + const val FACTORY_INITIALIZATION_FAILED = "F7009" + const val FACTORY_TIMEOUT = "F7010" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/LexerErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/LexerErrorCodes.kt new file mode 100644 index 00000000..a0904fb5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/LexerErrorCodes.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * ๋ ‰์„œ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object LexerErrorCodes { + val TOKENIZATION_FAILED = hs.kr.entrydsm.global.exception.ErrorCode.INVALID_TOKEN_SEQUENCE + val INVALID_CHARACTER = hs.kr.entrydsm.global.exception.ErrorCode.UNEXPECTED_CHARACTER + val UNEXPECTED_TOKEN = hs.kr.entrydsm.global.exception.ErrorCode.INVALID_TOKEN_SEQUENCE + val INVALID_NUMBER_FORMAT = hs.kr.entrydsm.global.exception.ErrorCode.INVALID_NUMBER_FORMAT + val INVALID_STRING_LITERAL = hs.kr.entrydsm.global.exception.ErrorCode.UNEXPECTED_CHARACTER + val UNCLOSED_STRING = hs.kr.entrydsm.global.exception.ErrorCode.UNCLOSED_VARIABLE + val INVALID_IDENTIFIER = hs.kr.entrydsm.global.exception.ErrorCode.UNEXPECTED_CHARACTER + val TOKEN_POSITION_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.INVALID_TOKEN_SEQUENCE + val LEXER_STATE_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.INVALID_TOKEN_SEQUENCE + val CHARACTER_ENCODING_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.UNEXPECTED_CHARACTER +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/ParserErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/ParserErrorCodes.kt new file mode 100644 index 00000000..1f5e56a3 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/ParserErrorCodes.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * ํŒŒ์„œ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object ParserErrorCodes { + val PARSING_FAILED = hs.kr.entrydsm.global.exception.ErrorCode.PARSING_ERROR + val SYNTAX_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.SYNTAX_ERROR + val UNEXPECTED_EOF = hs.kr.entrydsm.global.exception.ErrorCode.UNEXPECTED_END_OF_INPUT + val GRAMMAR_VIOLATION = hs.kr.entrydsm.global.exception.ErrorCode.GRAMMAR_CONFLICT + val LR_CONFLICT = hs.kr.entrydsm.global.exception.ErrorCode.LR_PARSING_ERROR + val SHIFT_REDUCE_CONFLICT = hs.kr.entrydsm.global.exception.ErrorCode.GRAMMAR_CONFLICT + val REDUCE_REDUCE_CONFLICT = hs.kr.entrydsm.global.exception.ErrorCode.GRAMMAR_CONFLICT + val INVALID_PRODUCTION = hs.kr.entrydsm.global.exception.ErrorCode.INVALID_AST_NODE + val PARSER_STATE_ERROR = hs.kr.entrydsm.global.exception.ErrorCode.LR_PARSING_ERROR + val AST_CONSTRUCTION_FAILED = hs.kr.entrydsm.global.exception.ErrorCode.AST_BUILD_ERROR +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/PolicyErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/PolicyErrorCodes.kt new file mode 100644 index 00000000..04216a5a --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/PolicyErrorCodes.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * ์ •์ฑ… ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object PolicyErrorCodes { + const val POLICY_VIOLATION = "P9001" + const val INVALID_POLICY = "P9002" + const val POLICY_CONFLICT = "P9003" + const val POLICY_EVALUATION_FAILED = "P9004" + const val UNSUPPORTED_POLICY_TYPE = "P9005" + const val POLICY_TIMEOUT = "P9006" + const val POLICY_DEPENDENCY_ERROR = "P9007" + const val POLICY_CONFIGURATION_ERROR = "P9008" + const val POLICY_ENFORCEMENT_FAILED = "P9009" + const val POLICY_CHAIN_ERROR = "P9010" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/SpecificationErrorCodes.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/SpecificationErrorCodes.kt new file mode 100644 index 00000000..60c8d80c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/constants/error/SpecificationErrorCodes.kt @@ -0,0 +1,20 @@ +package hs.kr.entrydsm.global.constants.error + +/** + * ๋ช…์„ธ ๊ด€๋ จ ์—๋Ÿฌ ์ฝ”๋“œ๋“ค์„ ์ •์˜ํ•˜๋Š” ์ƒ์ˆ˜ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.31 + */ +object SpecificationErrorCodes { + const val SPECIFICATION_FAILED = "S8001" + const val INVALID_SPECIFICATION = "S8002" + const val SPECIFICATION_COMPOSITION_FAILED = "S8003" + const val SPECIFICATION_EVALUATION_ERROR = "S8004" + const val UNSUPPORTED_SPECIFICATION_TYPE = "S8005" + const val SPECIFICATION_TIMEOUT = "S8006" + const val SPECIFICATION_DEPENDENCY_ERROR = "S8007" + const val SPECIFICATION_CACHE_ERROR = "S8008" + const val COMPLEX_SPECIFICATION_LIMIT = "S8009" + const val SPECIFICATION_VALIDATION_FAILED = "S8010" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/contract/VisitableContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/contract/VisitableContract.kt new file mode 100644 index 00000000..299c07c0 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/contract/VisitableContract.kt @@ -0,0 +1,46 @@ +package hs.kr.entrydsm.global.contract + +/** + * Visitor ํŒจํ„ด์—์„œ ๋ฐฉ๋ฌธ ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด๋ฅผ ์œ„ํ•œ ๊ณ„์•ฝ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * Visitor ํŒจํ„ด์˜ Element ์—ญํ• ์„ ํ•˜๋Š” ๊ฐ์ฒด๋“ค์ด ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋กœ, + * ๋ฐฉ๋ฌธ์ž(Visitor)๋ฅผ ๋ฐ›์•„๋“ค์ด๊ณ  ์ ์ ˆํ•œ ๋ฐฉ๋ฌธ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•˜๋Š” ์ฑ…์ž„์„ ๊ฐ€์ง‘๋‹ˆ๋‹ค. + * AST ๋…ธ๋“œ, ํ‘œํ˜„์‹ ๋“ฑ์—์„œ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @param R ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ ํƒ€์ž… + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +interface VisitableContract { + + /** + * ๋ฐฉ๋ฌธ์ž๋ฅผ ๋ฐ›์•„๋“ค์ด๊ณ  ์ ์ ˆํ•œ ๋ฐฉ๋ฌธ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. + * + * ๊ตฌํ˜„์ฒด๋Š” ์ž์‹ ์˜ ํƒ€์ž…์— ๋งž๋Š” visitor์˜ ๋ฐฉ๋ฌธ ๋ฉ”์„œ๋“œ๋ฅผ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + * ์ด๋ฅผ ํ†ตํ•ด Double Dispatch๊ฐ€ ๊ตฌํ˜„๋˜์–ด ๋Ÿฐํƒ€์ž„์— ์ •ํ™•ํ•œ ๋ฉ”์„œ๋“œ๊ฐ€ ์„ ํƒ๋ฉ๋‹ˆ๋‹ค. + * + * @param visitor ๋ฐฉ๋ฌธ์ž ๊ฐ์ฒด + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun accept(visitor: VisitorContract): R + + /** + * ๋ฐฉ๋ฌธ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * ํŠน์ • ์กฐ๊ฑดํ•˜์—์„œ๋งŒ ๋ฐฉ๋ฌธ์„ ํ—ˆ์šฉํ•˜๊ณ  ์‹ถ์€ ๊ฒฝ์šฐ ์ด ๋ฉ”์„œ๋“œ๋ฅผ ์˜ค๋ฒ„๋ผ์ด๋“œํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฐฉ๋ฌธ ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isVisitable(): Boolean = true + + /** + * ๋ฐฉ๋ฌธ์ž ํƒ€์ž…์ด ์ง€์›๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param visitorClass ํ™•์ธํ•  ๋ฐฉ๋ฌธ์ž ํด๋ž˜์Šค + * @return ์ง€์›๋˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun supportsVisitor(visitorClass: Class<*>): Boolean = true +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/contract/VisitorContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/contract/VisitorContract.kt new file mode 100644 index 00000000..5382aa37 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/contract/VisitorContract.kt @@ -0,0 +1,67 @@ +package hs.kr.entrydsm.global.contract + +/** + * Visitor ํŒจํ„ด์„ ์œ„ํ•œ ๊ธฐ๋ณธ ๊ณ„์•ฝ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ์„œ๋กœ ๋‹ค๋ฅธ ํƒ€์ž…์˜ ๊ฐ์ฒด๋“ค์— ๋Œ€ํ•ด ํƒ€์ž…๋ณ„๋กœ ๋‹ค๋ฅธ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•  ์ˆ˜ ์žˆ๋„๋ก + * ํ•˜๋Š” Visitor ํŒจํ„ด์˜ ๊ตฌํ˜„์„ ์œ„ํ•œ ๊ธฐ๋ณธ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * AST ์ˆœํšŒ, ํ‘œํ˜„์‹ ํ‰๊ฐ€ ๋“ฑ์—์„œ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค. + * + * @param T ๋ฐฉ๋ฌธํ•  ๋…ธ๋“œ์˜ ๊ธฐ๋ณธ ํƒ€์ž… + * @param R ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ ํƒ€์ž… + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +interface VisitorContract { + + /** + * ๊ธฐ๋ณธ ๋ฐฉ๋ฌธ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + * + * ํŠน์ • ํƒ€์ž…์— ๋Œ€ํ•œ ์ „์šฉ ๋ฐฉ๋ฌธ ๋ฉ”์„œ๋“œ๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ + * ์ด ๋ฉ”์„œ๋“œ๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ๋…ธ๋“œ + * @return ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun visit(node: T): R + + /** + * ์—ฌ๋Ÿฌ ๋…ธ๋“œ๋ฅผ ์ˆœ์ฐจ์ ์œผ๋กœ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodes ๋ฐฉ๋ฌธํ•  ๋…ธ๋“œ๋“ค + * @return ๊ฐ ๋…ธ๋“œ ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ + */ + fun visitAll(nodes: List): List = nodes.map { visit(it) } + + /** + * ํŠน์ • ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ๋…ธ๋“œ๋งŒ ๋ฐฉ๋ฌธํ•ฉ๋‹ˆ๋‹ค. + * + * @param nodes ๋ฐฉ๋ฌธํ•  ๋…ธ๋“œ๋“ค + * @param predicate ๋ฐฉ๋ฌธ ์กฐ๊ฑด + * @return ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š” ๋…ธ๋“œ๋“ค์˜ ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ ๋ฆฌ์ŠคํŠธ + */ + fun visitIf(nodes: List, predicate: (T) -> Boolean): List = + nodes.filter(predicate).map { visit(it) } + + /** + * ๋…ธ๋“œ ๋ฐฉ๋ฌธ ์ „์— ์‹คํ–‰๋˜๋Š” ์ „์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•  ๋…ธ๋“œ + */ + fun beforeVisit(node: T) { + // ๊ธฐ๋ณธ ๊ตฌํ˜„์€ ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š์Œ + } + + /** + * ๋…ธ๋“œ ๋ฐฉ๋ฌธ ํ›„์— ์‹คํ–‰๋˜๋Š” ํ›„์ฒ˜๋ฆฌ ๋ฉ”์„œ๋“œ์ž…๋‹ˆ๋‹ค. + * + * @param node ๋ฐฉ๋ฌธํ•œ ๋…ธ๋“œ + * @param result ๋ฐฉ๋ฌธ ๊ฒฐ๊ณผ + */ + fun afterVisit(node: T, result: R) { + // ๊ธฐ๋ณธ ๊ตฌํ˜„์€ ์•„๋ฌด๊ฒƒ๋„ ํ•˜์ง€ ์•Š์Œ + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/coordination/CrossDomainContract.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/coordination/CrossDomainContract.kt new file mode 100644 index 00000000..04676dfd --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/coordination/CrossDomainContract.kt @@ -0,0 +1,577 @@ +package hs.kr.entrydsm.global.coordination + +import hs.kr.entrydsm.global.interfaces.AntiCorruptionLayerMarker +import hs.kr.entrydsm.global.interfaces.DomainMarker +import hs.kr.entrydsm.global.annotation.DomainEvent +import hs.kr.entrydsm.global.values.Result +import java.time.Instant + +/** + * ๋„๋ฉ”์ธ ๊ฐ„ ๊ณ„์•ฝ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Cross-Domain Contract ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์„œ๋กœ ๋‹ค๋ฅธ ๋„๋ฉ”์ธ ๊ฐ„์˜ + * ์ƒํ˜ธ์ž‘์šฉ ๊ทœ์น™๊ณผ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ๊ฒฝ๊ณ„๋ฅผ + * ๋ช…ํ™•ํžˆ ํ•˜๊ณ , ์•ˆ์ „ํ•œ ๋„๋ฉ”์ธ ๊ฐ„ ํ†ต์‹ ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface CrossDomainContract : DomainMarker, AntiCorruptionLayerMarker { + + override fun getDomainContext(): String = "global" + + override fun getDomainType(): String = "cross-domain-contract" + + /** + * ๊ณ„์•ฝ์˜ ์†Œ์Šค ๋„๋ฉ”์ธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์†Œ์Šค ๋„๋ฉ”์ธ ์ด๋ฆ„ + */ + fun getSourceDomain(): String + + /** + * ๊ณ„์•ฝ์˜ ๋Œ€์ƒ ๋„๋ฉ”์ธ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋Œ€์ƒ ๋„๋ฉ”์ธ ์ด๋ฆ„ + */ + fun getTargetDomain(): String + + /** + * ๊ณ„์•ฝ ๋ฒ„์ „์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณ„์•ฝ ๋ฒ„์ „ + */ + fun getContractVersion(): String + + /** + * ๊ณ„์•ฝ์ด ์œ ํšจํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isContractValid(): Boolean + + /** + * ๊ณ„์•ฝ์˜ ํ˜ธํ™˜์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param otherContract ๋น„๊ตํ•  ๋‹ค๋ฅธ ๊ณ„์•ฝ + * @return ํ˜ธํ™˜๋˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isCompatibleWith(otherContract: CrossDomainContract): Boolean + + /** + * ์ง€์›๋˜๋Š” ์—ฐ์‚ฐ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์› ์—ฐ์‚ฐ ๋ชฉ๋ก + */ + fun getSupportedOperations(): Set + + /** + * ์ง€์›๋˜๋Š” ์ด๋ฒคํŠธ ํƒ€์ž…๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์› ์ด๋ฒคํŠธ ํƒ€์ž… ๋ชฉ๋ก + */ + fun getSupportedEventTypes(): Set + + /** + * ๊ณ„์•ฝ ์กฐ๊ฑด๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณ„์•ฝ ์กฐ๊ฑด ๋ชฉ๋ก + */ + fun getContractConditions(): List + + /** + * ๊ณ„์•ฝ ์ œ์•ฝ์‚ฌํ•ญ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ณ„์•ฝ ์ œ์•ฝ์‚ฌํ•ญ ๋ชฉ๋ก + */ + fun getContractConstraints(): List + + /** + * SLA (Service Level Agreement) ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return SLA ์ •๋ณด + */ + fun getServiceLevelAgreement(): ServiceLevelAgreement + + /** + * ๊ณ„์•ฝ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋งต + */ + fun getContractMetadata(): Map +} + +/** + * ๊ณ„์‚ฐ๊ธฐ ๋„๋ฉ”์ธ ๊ฐ„ ๊ณ„์•ฝ์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface CalculatorCrossDomainContract : CrossDomainContract { + + /** + * ๋ ‰์„œ ๋„๋ฉ”์ธ๊ณผ์˜ ๊ณ„์•ฝ์ž…๋‹ˆ๋‹ค. + */ + interface WithLexer : CalculatorCrossDomainContract { + + override fun getTargetDomain(): String = "lexer" + + override fun getSupportedOperations(): Set = setOf( + "tokenize", + "validateInput", + "getTokenTypes" + ) + + override fun getSupportedEventTypes(): Set = setOf( + "TOKENIZATION_COMPLETED", + "TOKENIZATION_FAILED", + "INPUT_VALIDATED" + ) + + /** + * ํ† ํฐํ™” ์š”์ฒญ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ํ† ํฐํ™” ์š”์ฒญ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateTokenizationRequest(request: TokenizationRequest): Result + + /** + * ํ† ํฐํ™” ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ํ† ํฐํ™” ๊ฒฐ๊ณผ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateTokenizationResult(result: TokenizationResult): Result + } + + /** + * ํŒŒ์„œ ๋„๋ฉ”์ธ๊ณผ์˜ ๊ณ„์•ฝ์ž…๋‹ˆ๋‹ค. + */ + interface WithParser : CalculatorCrossDomainContract { + + override fun getTargetDomain(): String = "parser" + + override fun getSupportedOperations(): Set = setOf( + "parse", + "validateGrammar", + "buildAST" + ) + + override fun getSupportedEventTypes(): Set = setOf( + "PARSING_COMPLETED", + "PARSING_FAILED", + "AST_BUILT" + ) + + /** + * ํŒŒ์‹ฑ ์š”์ฒญ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ํŒŒ์‹ฑ ์š”์ฒญ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateParsingRequest(request: ParsingRequest): Result + + /** + * ํŒŒ์‹ฑ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ํŒŒ์‹ฑ ๊ฒฐ๊ณผ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateParsingResult(result: ParsingResult): Result + } + + /** + * ํ‰๊ฐ€์ž ๋„๋ฉ”์ธ๊ณผ์˜ ๊ณ„์•ฝ์ž…๋‹ˆ๋‹ค. + */ + interface WithEvaluator : CalculatorCrossDomainContract { + + override fun getTargetDomain(): String = "evaluator" + + override fun getSupportedOperations(): Set = setOf( + "evaluate", + "validateVariables", + "executeFunction" + ) + + override fun getSupportedEventTypes(): Set = setOf( + "EVALUATION_COMPLETED", + "EVALUATION_FAILED", + "FUNCTION_EXECUTED" + ) + + /** + * ํ‰๊ฐ€ ์š”์ฒญ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ํ‰๊ฐ€ ์š”์ฒญ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateEvaluationRequest(request: EvaluationRequest): Result + + /** + * ํ‰๊ฐ€ ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ํ‰๊ฐ€ ๊ฒฐ๊ณผ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateEvaluationResult(result: EvaluationResult): Result + } + + /** + * ํ‘œํ˜„๊ธฐ ๋„๋ฉ”์ธ๊ณผ์˜ ๊ณ„์•ฝ์ž…๋‹ˆ๋‹ค. + */ + interface WithExpresser : CalculatorCrossDomainContract { + + override fun getTargetDomain(): String = "expresser" + + override fun getSupportedOperations(): Set = setOf( + "format", + "generateReport", + "export" + ) + + override fun getSupportedEventTypes(): Set = setOf( + "FORMATTING_COMPLETED", + "FORMATTING_FAILED", + "REPORT_GENERATED" + ) + + /** + * ํ˜•์‹ํ™” ์š”์ฒญ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ํ˜•์‹ํ™” ์š”์ฒญ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateFormattingRequest(request: FormattingRequest): Result + + /** + * ํ˜•์‹ํ™” ๊ฒฐ๊ณผ๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param result ํ˜•์‹ํ™” ๊ฒฐ๊ณผ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validateFormattingResult(result: FormattingResult): Result + } +} + +/** + * ๊ณ„์•ฝ ์กฐ๊ฑด์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class ContractCondition( + val id: String, + val name: String, + val description: String, + val type: ConditionType, + val parameters: Map = emptyMap(), + val mandatory: Boolean = true, + val priority: Int = 0 +) { + + enum class ConditionType { + PRECONDITION, // ์‚ฌ์ „ ์กฐ๊ฑด + POSTCONDITION, // ์‚ฌํ›„ ์กฐ๊ฑด + INVARIANT, // ๋ถˆ๋ณ€ ์กฐ๊ฑด + CONSTRAINT // ์ œ์•ฝ ์กฐ๊ฑด + } + + /** + * ์กฐ๊ฑด์„ ํ‰๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param context ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ + * @return ํ‰๊ฐ€ ๊ฒฐ๊ณผ + */ + fun evaluate(context: Map): Result { + return try { + val result = when (type) { + ConditionType.PRECONDITION -> evaluatePrecondition(context) + ConditionType.POSTCONDITION -> evaluatePostcondition(context) + ConditionType.INVARIANT -> evaluateInvariant(context) + ConditionType.CONSTRAINT -> evaluateConstraint(context) + } + Result.success(result) + } catch (e: Exception) { + Result.failure(ContractViolation.ConditionEvaluationError(id, e.message ?: "ํ‰๊ฐ€ ์‹คํŒจ")) + } + } + + private fun evaluatePrecondition(context: Map): Boolean { + // ์‚ฌ์ „ ์กฐ๊ฑด ํ‰๊ฐ€ ๋กœ์ง + return true // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } + + private fun evaluatePostcondition(context: Map): Boolean { + // ์‚ฌํ›„ ์กฐ๊ฑด ํ‰๊ฐ€ ๋กœ์ง + return true // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } + + private fun evaluateInvariant(context: Map): Boolean { + // ๋ถˆ๋ณ€ ์กฐ๊ฑด ํ‰๊ฐ€ ๋กœ์ง + return true // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } + + private fun evaluateConstraint(context: Map): Boolean { + // ์ œ์•ฝ ์กฐ๊ฑด ํ‰๊ฐ€ ๋กœ์ง + return true // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } +} + +/** + * ๊ณ„์•ฝ ์ œ์•ฝ์‚ฌํ•ญ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class ContractConstraint( + val id: String, + val name: String, + val description: String, + val type: ConstraintType, + val parameters: Map = emptyMap(), + val severity: Severity = Severity.ERROR +) { + + enum class ConstraintType { + RATE_LIMIT, // ํ˜ธ์ถœ ํšŸ์ˆ˜ ์ œํ•œ + SIZE_LIMIT, // ํฌ๊ธฐ ์ œํ•œ + TIME_LIMIT, // ์‹œ๊ฐ„ ์ œํ•œ + DEPENDENCY, // ์˜์กด์„ฑ ์ œ์•ฝ + SECURITY, // ๋ณด์•ˆ ์ œ์•ฝ + BUSINESS_RULE // ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ + } + + enum class Severity { + INFO, + WARNING, + ERROR, + CRITICAL + } + + /** + * ์ œ์•ฝ์‚ฌํ•ญ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param context ๊ฒ€์ฆ ์ปจํ…์ŠคํŠธ + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validate(context: Map): Result { + return try { + val violation = when (type) { + ConstraintType.RATE_LIMIT -> validateRateLimit(context) + ConstraintType.SIZE_LIMIT -> validateSizeLimit(context) + ConstraintType.TIME_LIMIT -> validateTimeLimit(context) + ConstraintType.DEPENDENCY -> validateDependency(context) + ConstraintType.SECURITY -> validateSecurity(context) + ConstraintType.BUSINESS_RULE -> validateBusinessRule(context) + } + + if (violation != null) { + Result.failure(violation) + } else { + Result.success(Unit) + } + } catch (e: Exception) { + Result.failure(ContractViolation.ConstraintValidationError(id, e.message ?: "๊ฒ€์ฆ ์‹คํŒจ")) + } + } + + private fun validateRateLimit(context: Map): ContractViolation? { + // ํ˜ธ์ถœ ํšŸ์ˆ˜ ์ œํ•œ ๊ฒ€์ฆ + return null // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } + + private fun validateSizeLimit(context: Map): ContractViolation? { + // ํฌ๊ธฐ ์ œํ•œ ๊ฒ€์ฆ + return null // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } + + private fun validateTimeLimit(context: Map): ContractViolation? { + // ์‹œ๊ฐ„ ์ œํ•œ ๊ฒ€์ฆ + return null // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } + + private fun validateDependency(context: Map): ContractViolation? { + // ์˜์กด์„ฑ ์ œ์•ฝ ๊ฒ€์ฆ + return null // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } + + private fun validateSecurity(context: Map): ContractViolation? { + // ๋ณด์•ˆ ์ œ์•ฝ ๊ฒ€์ฆ + return null // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } + + private fun validateBusinessRule(context: Map): ContractViolation? { + // ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ๊ฒ€์ฆ + return null // ๊ฐ„๋‹จํ•œ ์˜ˆ์‹œ + } +} + +/** + * ์„œ๋น„์Šค ์ˆ˜์ค€ ๊ณ„์•ฝ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class ServiceLevelAgreement( + val availability: Double = 0.99, // ๊ฐ€์šฉ์„ฑ (99%) + val responseTime: Long = 1000, // ์‘๋‹ต ์‹œ๊ฐ„ (1์ดˆ) + val throughput: Int = 1000, // ์ฒ˜๋ฆฌ๋Ÿ‰ (์ดˆ๋‹น 1000๊ฐœ) + val errorRate: Double = 0.01, // ์˜ค๋ฅ˜์œจ (1%) + val mttr: Long = 300000, // ํ‰๊ท  ๋ณต๊ตฌ ์‹œ๊ฐ„ (5๋ถ„) + val mtbf: Long = 86400000, // ํ‰๊ท  ์žฅ์•  ๊ฐ„๊ฒฉ (24์‹œ๊ฐ„) + val dataRetention: Long = 2592000000, // ๋ฐ์ดํ„ฐ ๋ณด์กด ๊ธฐ๊ฐ„ (30์ผ) + val backupFrequency: Long = 86400000 // ๋ฐฑ์—… ์ฃผ๊ธฐ (24์‹œ๊ฐ„) +) { + + /** + * SLA ๋ฉ”ํŠธ๋ฆญ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param metrics ์‹ค์ œ ๋ฉ”ํŠธ๋ฆญ + * @return SLA ์ค€์ˆ˜ ์—ฌ๋ถ€ + */ + fun checkCompliance(metrics: SLAMetrics): SLAComplianceResult { + val violations = mutableListOf() + + if (metrics.actualAvailability < availability) { + violations.add("๊ฐ€์šฉ์„ฑ: ${metrics.actualAvailability} < $availability") + } + + if (metrics.actualResponseTime > responseTime) { + violations.add("์‘๋‹ต์‹œ๊ฐ„: ${metrics.actualResponseTime}ms > ${responseTime}ms") + } + + if (metrics.actualThroughput < throughput) { + violations.add("์ฒ˜๋ฆฌ๋Ÿ‰: ${metrics.actualThroughput}/s < ${throughput}/s") + } + + if (metrics.actualErrorRate > errorRate) { + violations.add("์˜ค๋ฅ˜์œจ: ${metrics.actualErrorRate} > $errorRate") + } + + return SLAComplianceResult( + compliant = violations.isEmpty(), + violations = violations, + score = calculateComplianceScore(metrics) + ) + } + + private fun calculateComplianceScore(metrics: SLAMetrics): Double { + var score = 0.0 + var weight = 0.0 + + // ๊ฐ€์šฉ์„ฑ ์ ์ˆ˜ (๊ฐ€์ค‘์น˜: 30%) + if (metrics.actualAvailability >= availability) { + score += 30.0 + } else { + score += 30.0 * (metrics.actualAvailability / availability) + } + weight += 30.0 + + // ์‘๋‹ต์‹œ๊ฐ„ ์ ์ˆ˜ (๊ฐ€์ค‘์น˜: 25%) + if (metrics.actualResponseTime <= responseTime) { + score += 25.0 + } else { + score += 25.0 * (responseTime.toDouble() / metrics.actualResponseTime) + } + weight += 25.0 + + // ์ฒ˜๋ฆฌ๋Ÿ‰ ์ ์ˆ˜ (๊ฐ€์ค‘์น˜: 25%) + if (metrics.actualThroughput >= throughput) { + score += 25.0 + } else { + score += 25.0 * (metrics.actualThroughput.toDouble() / throughput) + } + weight += 25.0 + + // ์˜ค๋ฅ˜์œจ ์ ์ˆ˜ (๊ฐ€์ค‘์น˜: 20%) + if (metrics.actualErrorRate <= errorRate) { + score += 20.0 + } else { + score += 20.0 * (errorRate / metrics.actualErrorRate) + } + weight += 20.0 + + return (score / weight * 100).coerceIn(0.0, 100.0) + } +} + +/** + * SLA ๋ฉ”ํŠธ๋ฆญ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class SLAMetrics( + val actualAvailability: Double, + val actualResponseTime: Long, + val actualThroughput: Int, + val actualErrorRate: Double, + val measuredPeriod: Long = System.currentTimeMillis() +) + +/** + * SLA ์ค€์ˆ˜ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class SLAComplianceResult( + val compliant: Boolean, + val violations: List, + val score: Double, + val checkedAt: Instant = Instant.now() +) + +/** + * ๊ณ„์•ฝ ์œ„๋ฐ˜์„ ๋‚˜ํƒ€๋‚ด๋Š” sealed class์ž…๋‹ˆ๋‹ค. + */ +sealed class ContractViolation( + val message: String, + val code: String, + val severity: Severity = Severity.ERROR +) { + + enum class Severity { + INFO, WARNING, ERROR, CRITICAL + } + + /** + * ์กฐ๊ฑด ํ‰๊ฐ€ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class ConditionEvaluationError(val conditionId: String, val reason: String) : + ContractViolation("์กฐ๊ฑด ํ‰๊ฐ€ ์˜ค๋ฅ˜ [$conditionId]: $reason", "CONDITION_EVAL_ERROR") + + /** + * ์ œ์•ฝ์‚ฌํ•ญ ๊ฒ€์ฆ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class ConstraintValidationError(val constraintId: String, val reason: String) : + ContractViolation("์ œ์•ฝ์‚ฌํ•ญ ๊ฒ€์ฆ ์˜ค๋ฅ˜ [$constraintId]: $reason", "CONSTRAINT_VALIDATION_ERROR") + + /** + * SLA ์œ„๋ฐ˜์ž…๋‹ˆ๋‹ค. + */ + data class SLAViolation(val metric: String, val expected: String, val actual: String) : + ContractViolation("SLA ์œ„๋ฐ˜ [$metric]: ์˜ˆ์ƒ $expected, ์‹ค์ œ $actual", "SLA_VIOLATION") + + /** + * ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class VersionMismatch(val expectedVersion: String, val actualVersion: String) : + ContractViolation("๋ฒ„์ „ ๋ถˆ์ผ์น˜: ์˜ˆ์ƒ $expectedVersion, ์‹ค์ œ $actualVersion", "VERSION_MISMATCH") + + /** + * ์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž…๋‹ˆ๋‹ค. + */ + data class UnsupportedOperation(val operation: String) : + ContractViolation("์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ: $operation", "UNSUPPORTED_OPERATION") + + /** + * ๋ฐ์ดํ„ฐ ํ˜•์‹ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class DataFormatError(val field: String, val expectedFormat: String, val actualFormat: String) : + ContractViolation("๋ฐ์ดํ„ฐ ํ˜•์‹ ์˜ค๋ฅ˜ [$field]: ์˜ˆ์ƒ $expectedFormat, ์‹ค์ œ $actualFormat", "DATA_FORMAT_ERROR") + + /** + * ์ผ๋ฐ˜์ ์ธ ๊ณ„์•ฝ ์œ„๋ฐ˜์ž…๋‹ˆ๋‹ค. + */ + data class GeneralViolation(val reason: String, val errorCode: String = "GENERAL_VIOLATION") : + ContractViolation(reason, errorCode) +} + +// ์š”์ฒญ/์‘๋‹ต ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค๋“ค +data class TokenizationRequest(val input: String, val options: Map = emptyMap()) +data class TokenizationResult(val tokens: List>, val success: Boolean) + +data class ParsingRequest(val tokens: List>, val options: Map = emptyMap()) +data class ParsingResult(val ast: Map, val success: Boolean) + +data class EvaluationRequest(val ast: Map, val variables: Map = emptyMap()) +data class EvaluationResult(val result: Any?, val success: Boolean) + +data class FormattingRequest(val data: Any, val format: String, val options: Map = emptyMap()) +data class FormattingResult(val formatted: String, val success: Boolean) \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/coordination/DomainCoordinator.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/coordination/DomainCoordinator.kt new file mode 100644 index 00000000..26386ef3 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/coordination/DomainCoordinator.kt @@ -0,0 +1,591 @@ +package hs.kr.entrydsm.global.coordination + +import hs.kr.entrydsm.global.interfaces.DomainMarker +import hs.kr.entrydsm.global.annotation.DomainEvent +import hs.kr.entrydsm.global.values.Result +import java.util.concurrent.ConcurrentLinkedQueue +import java.time.Instant + +/** + * ๋„๋ฉ”์ธ ๊ฐ„ ํ˜‘๋ ฅ์„ ์กฐ์œจํ•˜๋Š” ์ฝ”๋””๋„ค์ดํ„ฐ์ž…๋‹ˆ๋‹ค. + * + * DDD Domain Coordination ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์—ฌ๋Ÿฌ ๋„๋ฉ”์ธ ๊ฐ„์˜ ์ƒํ˜ธ์ž‘์šฉ์„ + * ์กฐ์œจํ•˜๊ณ  ๊ด€๋ฆฌํ•ฉ๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ์˜ ๋ฐœํ–‰๊ณผ ๊ตฌ๋…, ๋„๋ฉ”์ธ ๊ฐ„ ํ†ต์‹ , + * ํŠธ๋žœ์žญ์…˜ ๊ฒฝ๊ณ„ ๊ด€๋ฆฌ ๋“ฑ์„ ๋‹ด๋‹นํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface DomainCoordinator : DomainMarker { + + override fun getDomainContext(): String = "global" + + override fun getDomainType(): String = "coordinator" + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param event ๋ฐœํ–‰ํ•  ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ + * @return ๋ฐœํ–‰ ๊ฒฐ๊ณผ + */ + fun publishEvent(event: DomainEvent): Result + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @param event ๋ฐœํ–‰ํ•  ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ + * @return ๋ฐœํ–‰ ๊ฒฐ๊ณผ + */ + fun publishEventSync(event: DomainEvent): Result + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋“ค์„ ์ผ๊ด„ ๋ฐœํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param events ๋ฐœํ–‰ํ•  ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋“ค + * @return ๋ฐœํ–‰ ๊ฒฐ๊ณผ + */ + fun publishEvents(events: List): Result + + /** + * ํŠน์ • ํƒ€์ž…์˜ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค. + * + * @param eventType ๊ตฌ๋…ํ•  ์ด๋ฒคํŠธ ํƒ€์ž… + * @param handler ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๊ธฐ + * @return ๊ตฌ๋… ID + */ + fun subscribeToEvent(eventType: String, handler: EventHandler): Result + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๊ตฌ๋…์„ ์ทจ์†Œํ•ฉ๋‹ˆ๋‹ค. + * + * @param subscriptionId ๊ตฌ๋… ID + * @return ์ทจ์†Œ ๊ฒฐ๊ณผ + */ + fun unsubscribeFromEvent(subscriptionId: String): Result + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param eventType ์ด๋ฒคํŠธ ํƒ€์ž… (null์ด๋ฉด ๋ชจ๋“  ์ด๋ฒคํŠธ) + * @return ์ด๋ฒคํŠธ ์ŠคํŠธ๋ฆผ + */ + fun getEventStream(eventType: String? = null): List + + /** + * ๋„๋ฉ”์ธ ๊ฐ„ ๋ฉ”์‹œ์ง€๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค. + * + * @param message ์ „์†กํ•  ๋ฉ”์‹œ์ง€ + * @return ์ „์†ก ๊ฒฐ๊ณผ + */ + fun sendMessage(message: DomainMessage): Result + + /** + * ๋„๋ฉ”์ธ ๊ฐ„ ์š”์ฒญ-์‘๋‹ต ํ†ต์‹ ์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param request ์š”์ฒญ ๋ฉ”์‹œ์ง€ + * @param timeout ํƒ€์ž„์•„์›ƒ (๋ฐ€๋ฆฌ์ดˆ) + * @return ์‘๋‹ต ๋ฉ”์‹œ์ง€ + */ + fun requestResponse(request: DomainMessage, timeout: Long = 30000): Result + + /** + * ํฌ๋กœ์Šค ๋„๋ฉ”์ธ ํŠธ๋žœ์žญ์…˜์„ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @param transactionName ํŠธ๋žœ์žญ์…˜ ์ด๋ฆ„ + * @param participants ์ฐธ์—ฌ ๋„๋ฉ”์ธ๋“ค + * @return ํŠธ๋žœ์žญ์…˜ ID + */ + fun beginCrossDomainTransaction( + transactionName: String, + participants: Set + ): Result + + /** + * ํฌ๋กœ์Šค ๋„๋ฉ”์ธ ํŠธ๋žœ์žญ์…˜์„ ์ปค๋ฐ‹ํ•ฉ๋‹ˆ๋‹ค. + * + * @param transactionId ํŠธ๋žœ์žญ์…˜ ID + * @return ์ปค๋ฐ‹ ๊ฒฐ๊ณผ + */ + fun commitCrossDomainTransaction(transactionId: String): Result + + /** + * ํฌ๋กœ์Šค ๋„๋ฉ”์ธ ํŠธ๋žœ์žญ์…˜์„ ๋กค๋ฐฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param transactionId ํŠธ๋žœ์žญ์…˜ ID + * @return ๋กค๋ฐฑ ๊ฒฐ๊ณผ + */ + fun rollbackCrossDomainTransaction(transactionId: String): Result + + /** + * ๋„๋ฉ”์ธ ์ƒํƒœ๋ฅผ ๋™๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param sourceDomain ์†Œ์Šค ๋„๋ฉ”์ธ + * @param targetDomain ๋Œ€์ƒ ๋„๋ฉ”์ธ + * @param syncRequest ๋™๊ธฐํ™” ์š”์ฒญ + * @return ๋™๊ธฐํ™” ๊ฒฐ๊ณผ + */ + fun synchronizeDomains( + sourceDomain: String, + targetDomain: String, + syncRequest: SynchronizationRequest + ): Result + + /** + * ๋„๋ฉ”์ธ ํ—ฌ์Šค ์ฒดํฌ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param domainName ์ฒดํฌํ•  ๋„๋ฉ”์ธ ์ด๋ฆ„ + * @return ํ—ฌ์Šค ์ฒดํฌ ๊ฒฐ๊ณผ + */ + fun checkDomainHealth(domainName: String): Result + + /** + * ๋ชจ๋“  ๋„๋ฉ”์ธ์˜ ํ—ฌ์Šค ์ฒดํฌ๋ฅผ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ „์ฒด ํ—ฌ์Šค ์ฒดํฌ ๊ฒฐ๊ณผ + */ + fun checkAllDomainsHealth(): Result, CoordinationError> + + /** + * ๋„๋ฉ”์ธ ํ† ํด๋กœ์ง€๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ๊ฐ„ ์˜์กด์„ฑ ์ •๋ณด + */ + fun getDomainTopology(): Result + + /** + * ์ฝ”๋””๋„ค์ดํ„ฐ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด + */ + fun getStatistics(): Result + + /** + * ์ฝ”๋””๋„ค์ดํ„ฐ๋ฅผ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹œ์ž‘ ๊ฒฐ๊ณผ + */ + fun start(): Result + + /** + * ์ฝ”๋””๋„ค์ดํ„ฐ๋ฅผ ์ค‘์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ค‘์ง€ ๊ฒฐ๊ณผ + */ + fun stop(): Result + + /** + * ์ฝ”๋””๋„ค์ดํ„ฐ๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹คํ–‰ ์ค‘์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isRunning(): Boolean +} + +/** + * ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +fun interface EventHandler { + + /** + * ์ด๋ฒคํŠธ๋ฅผ ์ฒ˜๋ฆฌํ•ฉ๋‹ˆ๋‹ค. + * + * @param event ์ฒ˜๋ฆฌํ•  ์ด๋ฒคํŠธ + * @return ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ + */ + fun handle(event: DomainEvent): Result +} + +/** + * ๋„๋ฉ”์ธ ๊ฐ„ ๋ฉ”์‹œ์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class DomainMessage( + val messageId: String, + val sourceDomain: String, + val targetDomain: String, + val messageType: String, + val payload: Map, + val headers: Map = emptyMap(), + val timestamp: Instant = Instant.now(), + val correlationId: String? = null, + val replyTo: String? = null, + val ttl: Long? = null // Time To Live in milliseconds +) { + + /** + * ์‘๋‹ต ๋ฉ”์‹œ์ง€๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param responsePayload ์‘๋‹ต ํŽ˜์ด๋กœ๋“œ + * @param responseType ์‘๋‹ต ๋ฉ”์‹œ์ง€ ํƒ€์ž… + * @return ์‘๋‹ต ๋ฉ”์‹œ์ง€ + */ + fun createResponse( + responsePayload: Map, + responseType: String = "RESPONSE" + ): DomainMessage = DomainMessage( + messageId = java.util.UUID.randomUUID().toString(), + sourceDomain = targetDomain, + targetDomain = sourceDomain, + messageType = responseType, + payload = responsePayload, + correlationId = messageId, + timestamp = Instant.now() + ) + + /** + * ๋ฉ”์‹œ์ง€๊ฐ€ ๋งŒ๋ฃŒ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋งŒ๋ฃŒ๋˜์—ˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isExpired(): Boolean = ttl?.let { + Instant.now().toEpochMilli() - timestamp.toEpochMilli() > it + } ?: false + + /** + * ๋ฉ”์‹œ์ง€์— ํ—ค๋”๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ํ—ค๋” ํ‚ค + * @param value ํ—ค๋” ๊ฐ’ + * @return ์ƒˆ๋กœ์šด DomainMessage + */ + fun withHeader(key: String, value: String): DomainMessage = + copy(headers = headers + (key to value)) + + /** + * ๋ฉ”์‹œ์ง€์— ์ƒ๊ด€๊ด€๊ณ„ ID๋ฅผ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @param correlationId ์ƒ๊ด€๊ด€๊ณ„ ID + * @return ์ƒˆ๋กœ์šด DomainMessage + */ + fun withCorrelationId(correlationId: String): DomainMessage = + copy(correlationId = correlationId) +} + +/** + * ๋™๊ธฐํ™” ์š”์ฒญ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class SynchronizationRequest( + val syncType: SyncType, + val entities: Set = emptySet(), + val filter: Map = emptyMap(), + val options: SyncOptions = SyncOptions() +) { + + enum class SyncType { + FULL, // ์ „์ฒด ๋™๊ธฐํ™” + INCREMENTAL, // ์ฆ๋ถ„ ๋™๊ธฐํ™” + SELECTIVE // ์„ ํƒ์  ๋™๊ธฐํ™” + } +} + +/** + * ๋™๊ธฐํ™” ์˜ต์…˜์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class SyncOptions( + val batchSize: Int = 100, + val timeout: Long = 300000, // 5๋ถ„ + val conflictResolution: ConflictResolution = ConflictResolution.SOURCE_WINS, + val validateData: Boolean = true, + val continueOnError: Boolean = false +) { + + enum class ConflictResolution { + SOURCE_WINS, // ์†Œ์Šค ์šฐ์„  + TARGET_WINS, // ๋Œ€์ƒ ์šฐ์„  + MERGE, // ๋ณ‘ํ•ฉ + MANUAL // ์ˆ˜๋™ ํ•ด๊ฒฐ + } +} + +/** + * ๋™๊ธฐํ™” ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class SynchronizationResult( + val success: Boolean, + val syncedCount: Int, + val failedCount: Int, + val conflicts: List = emptyList(), + val errors: List = emptyList(), + val startTime: Instant, + val endTime: Instant, + val duration: Long = endTime.toEpochMilli() - startTime.toEpochMilli() +) { + + /** + * ๋™๊ธฐํ™”๊ฐ€ ๋ถ€๋ถ„์ ์œผ๋กœ ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ถ€๋ถ„ ์„ฑ๊ณต์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isPartialSuccess(): Boolean = syncedCount > 0 && failedCount > 0 + + /** + * ์ „์ฒด ์ฒ˜๋ฆฌ๋œ ํ•ญ๋ชฉ ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ „์ฒด ํ•ญ๋ชฉ ์ˆ˜ + */ + fun getTotalProcessed(): Int = syncedCount + failedCount + + /** + * ์„ฑ๊ณต๋ฅ ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณต๋ฅ  (0.0 ~ 1.0) + */ + fun getSuccessRate(): Double { + val total = getTotalProcessed() + return if (total > 0) syncedCount.toDouble() / total else 0.0 + } +} + +/** + * ๋™๊ธฐํ™” ์ถฉ๋Œ์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class SyncConflict( + val entityId: String, + val entityType: String, + val conflictType: ConflictType, + val sourceValue: Any?, + val targetValue: Any?, + val resolution: ConflictResolution? = null +) { + + enum class ConflictType { + VERSION_MISMATCH, // ๋ฒ„์ „ ๋ถˆ์ผ์น˜ + DATA_MISMATCH, // ๋ฐ์ดํ„ฐ ๋ถˆ์ผ์น˜ + TYPE_MISMATCH, // ํƒ€์ž… ๋ถˆ์ผ์น˜ + CONSTRAINT_VIOLATION // ์ œ์•ฝ ์กฐ๊ฑด ์œ„๋ฐ˜ + } + + enum class ConflictResolution { + USE_SOURCE, // ์†Œ์Šค ์‚ฌ์šฉ + USE_TARGET, // ๋Œ€์ƒ ์‚ฌ์šฉ + MERGE, // ๋ณ‘ํ•ฉ + SKIP // ๊ฑด๋„ˆ๋›ฐ๊ธฐ + } +} + +/** + * ๋„๋ฉ”์ธ ํ—ฌ์Šค ์ƒํƒœ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class DomainHealthStatus( + val domainName: String, + val status: HealthStatus, + val lastChecked: Instant = Instant.now(), + val responseTime: Long = 0, + val details: Map = emptyMap(), + val errors: List = emptyList() +) { + + enum class HealthStatus { + HEALTHY, // ์ •์ƒ + DEGRADED, // ์„ฑ๋Šฅ ์ €ํ•˜ + UNHEALTHY, // ๋น„์ •์ƒ + UNKNOWN // ์•Œ ์ˆ˜ ์—†์Œ + } + + /** + * ๋„๋ฉ”์ธ์ด ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAvailable(): Boolean = status in setOf(HealthStatus.HEALTHY, HealthStatus.DEGRADED) + + /** + * ์‘๋‹ต ์‹œ๊ฐ„์ด ์ž„๊ณ„๊ฐ’์„ ์ดˆ๊ณผํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param threshold ์ž„๊ณ„๊ฐ’ (๋ฐ€๋ฆฌ์ดˆ) + * @return ์ดˆ๊ณผํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isResponseTimeSlow(threshold: Long = 1000): Boolean = responseTime > threshold +} + +/** + * ๋„๋ฉ”์ธ ํ† ํด๋กœ์ง€๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class DomainTopology( + val domains: Set, + val dependencies: Map>, + val communicationPatterns: Map, + val lastUpdated: Instant = Instant.now() +) { + + /** + * ๋„๋ฉ”์ธ์˜ ์˜์กด์„ฑ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param domainName ๋„๋ฉ”์ธ ์ด๋ฆ„ + * @return ์˜์กดํ•˜๋Š” ๋„๋ฉ”์ธ๋“ค + */ + fun getDependencies(domainName: String): Set = dependencies[domainName] ?: emptySet() + + /** + * ๋„๋ฉ”์ธ์— ์˜์กดํ•˜๋Š” ๋‹ค๋ฅธ ๋„๋ฉ”์ธ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param domainName ๋„๋ฉ”์ธ ์ด๋ฆ„ + * @return ์ด ๋„๋ฉ”์ธ์— ์˜์กดํ•˜๋Š” ๋„๋ฉ”์ธ๋“ค + */ + fun getDependents(domainName: String): Set = + dependencies.filterValues { it.contains(domainName) }.keys + + /** + * ์ˆœํ™˜ ์˜์กด์„ฑ์ด ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆœํ™˜ ์˜์กด์„ฑ์ด ์žˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasCircularDependencies(): Boolean { + // ๊ฐ„๋‹จํ•œ ์ˆœํ™˜ ์˜์กด์„ฑ ๊ฒ€์‚ฌ (DFS ๊ธฐ๋ฐ˜) + val visited = mutableSetOf() + val recursionStack = mutableSetOf() + + fun dfs(domain: String): Boolean { + visited.add(domain) + recursionStack.add(domain) + + dependencies[domain]?.forEach { dependency -> + if (!visited.contains(dependency)) { + if (dfs(dependency)) return true + } else if (recursionStack.contains(dependency)) { + return true + } + } + + recursionStack.remove(domain) + return false + } + + domains.forEach { domain -> + if (!visited.contains(domain)) { + if (dfs(domain)) return true + } + } + + return false + } +} + +/** + * ํ†ต์‹  ํŒจํ„ด์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class CommunicationPattern( + val patternType: PatternType, + val frequency: Int = 0, + val averageResponseTime: Long = 0, + val errorRate: Double = 0.0, + val lastUsed: Instant = Instant.now() +) { + + enum class PatternType { + SYNCHRONOUS, // ๋™๊ธฐ ํ†ต์‹  + ASYNCHRONOUS, // ๋น„๋™๊ธฐ ํ†ต์‹  + EVENT_DRIVEN, // ์ด๋ฒคํŠธ ์ฃผ๋„ + MESSAGE_QUEUE // ๋ฉ”์‹œ์ง€ ํ + } +} + +/** + * ์ฝ”๋””๋„ค์ดํ„ฐ ํ†ต๊ณ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class CoordinatorStatistics( + val totalEvents: Long = 0, + val totalMessages: Long = 0, + val activeSubscriptions: Int = 0, + val activeTransactions: Int = 0, + val averageEventProcessingTime: Long = 0, + val averageMessageProcessingTime: Long = 0, + val errorCount: Long = 0, + val uptime: Long = 0, + val lastReset: Instant = Instant.now() +) { + + /** + * ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ์œจ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ดˆ๋‹น ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ์ˆ˜ + */ + fun getEventThroughput(): Double { + val uptimeSeconds = uptime / 1000.0 + return if (uptimeSeconds > 0) totalEvents / uptimeSeconds else 0.0 + } + + /** + * ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ์œจ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ดˆ๋‹น ๋ฉ”์‹œ์ง€ ์ฒ˜๋ฆฌ ์ˆ˜ + */ + fun getMessageThroughput(): Double { + val uptimeSeconds = uptime / 1000.0 + return if (uptimeSeconds > 0) totalMessages / uptimeSeconds else 0.0 + } + + /** + * ์˜ค๋ฅ˜์œจ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ค๋ฅ˜์œจ (0.0 ~ 1.0) + */ + fun getErrorRate(): Double { + val totalOperations = totalEvents + totalMessages + return if (totalOperations > 0) errorCount.toDouble() / totalOperations else 0.0 + } +} + +/** + * ์ฝ”๋””๋„ค์ด์…˜ ์˜ค๋ฅ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” sealed class์ž…๋‹ˆ๋‹ค. + */ +sealed class CoordinationError( + val message: String, + val cause: Throwable? = null +) { + + /** + * ์ด๋ฒคํŠธ ๋ฐœํ–‰ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class EventPublishError(val eventType: String, val reason: String, val throwable: Throwable? = null) : + CoordinationError("์ด๋ฒคํŠธ ๋ฐœํ–‰ ์˜ค๋ฅ˜ [$eventType]: $reason", throwable) + + /** + * ์ด๋ฒคํŠธ ๊ตฌ๋… ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class EventSubscriptionError(val eventType: String, val reason: String) : + CoordinationError("์ด๋ฒคํŠธ ๊ตฌ๋… ์˜ค๋ฅ˜ [$eventType]: $reason") + + /** + * ๋ฉ”์‹œ์ง€ ์ „์†ก ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class MessageSendError(val targetDomain: String, val reason: String, val throwable: Throwable? = null) : + CoordinationError("๋ฉ”์‹œ์ง€ ์ „์†ก ์˜ค๋ฅ˜ [$targetDomain]: $reason", throwable) + + /** + * ํŠธ๋žœ์žญ์…˜ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class TransactionError(val transactionId: String, val reason: String, val throwable: Throwable? = null) : + CoordinationError("ํŠธ๋žœ์žญ์…˜ ์˜ค๋ฅ˜ [$transactionId]: $reason", throwable) + + /** + * ๋™๊ธฐํ™” ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class SynchronizationError(val sourceDomain: String, val targetDomain: String, val reason: String) : + CoordinationError("๋™๊ธฐํ™” ์˜ค๋ฅ˜ [$sourceDomain -> $targetDomain]: $reason") + + /** + * ๋„๋ฉ”์ธ ์—ฐ๊ฒฐ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class DomainConnectionError(val domainName: String, val reason: String, val throwable: Throwable? = null) : + CoordinationError("๋„๋ฉ”์ธ ์—ฐ๊ฒฐ ์˜ค๋ฅ˜ [$domainName]: $reason", throwable) + + /** + * ์„ค์ • ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class ConfigurationError(val reason: String) : + CoordinationError("์„ค์ • ์˜ค๋ฅ˜: $reason") + + /** + * ํƒ€์ž„์•„์›ƒ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class TimeoutError(val operation: String, val timeout: Long) : + CoordinationError("ํƒ€์ž„์•„์›ƒ ์˜ค๋ฅ˜ [$operation]: ${timeout}ms") + + /** + * ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class UnknownError(val reason: String, val throwable: Throwable? = null) : + CoordinationError("์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜: $reason", throwable) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/BusinessRuleException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/BusinessRuleException.kt new file mode 100644 index 00000000..1debf6ac --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/BusinessRuleException.kt @@ -0,0 +1,114 @@ +package hs.kr.entrydsm.global.exception + +import hs.kr.entrydsm.global.exception.ErrorCode +import hs.kr.entrydsm.global.exception.DomainException + +/** + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ์œ„๋ฐ˜ ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ์ž…๋‹ˆ๋‹ค. + * + * ๋„๋ฉ”์ธ์˜ ํ•ต์‹ฌ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง์ด๋‚˜ ์ •์ฑ…์„ ์œ„๋ฐ˜ํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๋กœ, + * ๋‹จ์ˆœํ•œ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์™€๋Š” ๋‹ฌ๋ฆฌ ๋ณต์žกํ•œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์ด๋‚˜ ์ œ์•ฝ์‚ฌํ•ญ์„ + * ์œ„๋ฐ˜ํ•œ ๊ฒฝ์šฐ์— ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์œ„๋ฐ˜๋œ ๊ทœ์น™์˜ ์ด๋ฆ„๊ณผ ์ƒ์„ธ ์ •๋ณด๋ฅผ ํฌํ•จํ•ฉ๋‹ˆ๋‹ค. + * + * @property ruleName ์œ„๋ฐ˜๋œ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์˜ ์ด๋ฆ„ + * @property ruleDescription ๊ทœ์น™์— ๋Œ€ํ•œ ์ƒ์„ธ ์„ค๋ช… (์„ ํƒ์‚ฌํ•ญ) + * @property context ๊ทœ์น™ ์œ„๋ฐ˜์ด ๋ฐœ์ƒํ•œ ์ปจํ…์ŠคํŠธ ์ •๋ณด (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +class BusinessRuleException( + errorCode: ErrorCode = ErrorCode.BUSINESS_RULE_VIOLATION, + val ruleName: String, + val ruleDescription: String? = null, + val ruleContext: Map = emptyMap(), + message: String = buildBusinessRuleMessage(errorCode, ruleName, ruleDescription), + cause: Throwable? = null +) : DomainException(errorCode, message, cause) { + + companion object { + /** + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ์œ„๋ฐ˜ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param errorCode ์˜ค๋ฅ˜ ์ฝ”๋“œ + * @param ruleName ์œ„๋ฐ˜๋œ ๊ทœ์น™๋ช… + * @param ruleDescription ๊ทœ์น™ ์„ค๋ช… + * @return ๊ตฌ์„ฑ๋œ ๋ฉ”์‹œ์ง€ + */ + private fun buildBusinessRuleMessage( + errorCode: ErrorCode, + ruleName: String, + ruleDescription: String? + ): String { + val baseMessage = errorCode.description + val ruleInfo = if (ruleDescription != null) { + "๊ทœ์น™: $ruleName ($ruleDescription)" + } else { + "๊ทœ์น™: $ruleName" + } + + return "$baseMessage - $ruleInfo" + } + } + + /** + * ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ์œ„๋ฐ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ทœ์น™๋ช…, ์„ค๋ช…, ์ปจํ…์ŠคํŠธ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋งต + */ + fun getBusinessRuleInfo(): Map { + val info = mutableMapOf( + "ruleName" to ruleName + ) + + ruleDescription?.let { info["ruleDescription"] = it } + + if (context.isNotEmpty()) { + info["context"] = context + } + + return info + } + + /** + * ํŠน์ • ์ปจํ…์ŠคํŠธ ๊ฐ’์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ์กฐํšŒํ•  ์ปจํ…์ŠคํŠธ ํ‚ค + * @return ์ปจํ…์ŠคํŠธ ๊ฐ’ (์—†์œผ๋ฉด null) + */ + fun getContextValue(key: String): Any? = context[key] + + /** + * ์ปจํ…์ŠคํŠธ์— ํŠน์ • ํ‚ค๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ํ™•์ธํ•  ํ‚ค + * @return ์กด์žฌํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun hasContextKey(key: String): Boolean = context.containsKey(key) + + /** + * ์ „์ฒด ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ์ •๋ณด์™€ ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ์ •๋ณด๊ฐ€ ๊ฒฐํ•ฉ๋œ ๋งต + */ + fun getFullErrorInfo(): Map { + val baseInfo = super.toErrorInfo().toMutableMap() + val businessRuleInfo = getBusinessRuleInfo() + + businessRuleInfo.forEach { (key, value) -> + when (value) { + is Map<*, *> -> baseInfo[key] = value.toString() + else -> baseInfo[key] = value?.toString() ?: "" + } + } + + return baseInfo + } + + override fun toString(): String { + val businessRuleDetails = getBusinessRuleInfo() + return "${super.toString()}, businessRule=${businessRuleDetails}" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/DomainException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/DomainException.kt new file mode 100644 index 00000000..205cb78c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/DomainException.kt @@ -0,0 +1,70 @@ +package hs.kr.entrydsm.global.exception + +/** + * ๋„๋ฉ”์ธ ๊ณ„์ธต์—์„œ ๋ฐœ์ƒํ•˜๋Š” ๋ชจ๋“  ์˜ˆ์™ธ์˜ ๊ธฐ๋ณธ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD(Domain-Driven Design) ์•„ํ‚คํ…์ฒ˜์—์„œ ๋„๋ฉ”์ธ ๊ทœ์น™ ์œ„๋ฐ˜์ด๋‚˜ ๋น„์ฆˆ๋‹ˆ์Šค ๋กœ์ง ์˜ค๋ฅ˜ ๋“ฑ + * ๋„๋ฉ”์ธ ๊ณ„์ธต์—์„œ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์˜ˆ์™ธ ์ƒํ™ฉ์„ ์ฒ˜๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ๊ธฐ๋ณธ ์˜ˆ์™ธ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * ๋ชจ๋“  ๋„๋ฉ”์ธ ํŠนํ™” ์˜ˆ์™ธ๋Š” ์ด ํด๋ž˜์Šค๋ฅผ ์ƒ์†๋ฐ›์•„ ๊ตฌํ˜„๋ฉ๋‹ˆ๋‹ค. + * + * @property errorCode ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜์˜ ์ฝ”๋“œ ์ •๋ณด + * @property message ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ (๊ธฐ๋ณธ๊ฐ’: errorCode.description) + * @property cause ์›์ธ์ด ๋˜๋Š” ์˜ˆ์™ธ (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +open class DomainException( + val errorCode: ErrorCode, + message: String = errorCode.description, + cause: Throwable? = null, + val context: Map = emptyMap() +) : RuntimeException(message, cause) { + + /** + * ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ค๋ฅ˜ ์ฝ”๋“œ ๋ฌธ์ž์—ด (์˜ˆ: LEX001, PAR002) + */ + fun getCode(): String = errorCode.code + + /** + * ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•œ ๋„๋ฉ”์ธ ์ ‘๋‘์‚ฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ์ ‘๋‘์‚ฌ (์˜ˆ: LEX, PAR, AST) + */ + fun getDomain(): String = errorCode.getDomainPrefix() + + /** + * ์˜ค๋ฅ˜ ๋ฒˆํ˜ธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ค๋ฅ˜ ๋ฒˆํ˜ธ (์˜ˆ: 001, 002) + */ + fun getErrorNumber(): String = errorCode.getErrorNumber() + + /** + * ํŠน์ • ๋„๋ฉ”์ธ์—์„œ ๋ฐœ์ƒํ•œ ์˜ค๋ฅ˜์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param domainPrefix ํ™•์ธํ•  ๋„๋ฉ”์ธ ์ ‘๋‘์‚ฌ + * @return ํ•ด๋‹น ๋„๋ฉ”์ธ ์˜ค๋ฅ˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isFromDomain(domainPrefix: String): Boolean = errorCode.belongsToDomain(domainPrefix) + + /** + * ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ค๋ฅ˜ ์ฝ”๋“œ, ๋ฉ”์‹œ์ง€, ๋„๋ฉ”์ธ ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋งต + */ + fun toErrorInfo(): Map = mapOf( + "code" to getCode(), + "message" to (message ?: ""), + "domain" to getDomain(), + "errorNumber" to getErrorNumber() + ) + + override fun toString(): String { + return "${this::class.simpleName}(code=${getCode()}, domain=${getDomain()}, message=$message)" + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/ErrorCode.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/ErrorCode.kt new file mode 100644 index 00000000..c93eca8d --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/ErrorCode.kt @@ -0,0 +1,416 @@ +package hs.kr.entrydsm.global.exception + +/** + * ์‹œ์Šคํ…œ ์ „๋ฐ˜์—์„œ ์‚ฌ์šฉ๋˜๋Š” ๋„๋ฉ”์ธ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ์ •์˜ํ•˜๋Š” ์—ด๊ฑฐํ˜•์ž…๋‹ˆ๋‹ค. + * + * ๊ฐ ๋„๋ฉ”์ธ๋ณ„๋กœ ๊ณ ์œ ํ•œ ์˜ค๋ฅ˜ ์ฝ”๋“œ๋ฅผ ๊ฐ€์ง€๋ฉฐ, ์˜ค๋ฅ˜์˜ ์›์ธ๊ณผ ํ•ด๊ฒฐ ๋ฐฉ์•ˆ์„ ๋ช…ํ™•ํžˆ ์‹๋ณ„ํ•  ์ˆ˜ ์žˆ๋„๋ก + * ์ฒด๊ณ„์ ์œผ๋กœ ๋ถ„๋ฅ˜๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. ์ฝ”๋“œ ์ฒด๊ณ„๋Š” ๋„๋ฉ”์ธ๋ณ„ ์ ‘๋‘์‚ฌ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @property code ๊ณ ์œ ํ•œ ์˜ค๋ฅ˜ ์ฝ”๋“œ (์˜ˆ: LEX001, PAR002) + * @property description ์˜ค๋ฅ˜์— ๋Œ€ํ•œ ํ•œ๊ตญ์–ด ์„ค๋ช… + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +enum class ErrorCode(val code: String, val description: String) { + // ๊ณตํ†ต ์˜ค๋ฅ˜ (CMN) + UNKNOWN_ERROR("CMN001", "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + VALIDATION_FAILED("CMN002", "์œ ํšจ์„ฑ ๊ฒ€์‚ฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + BUSINESS_RULE_VIOLATION("CMN003", "๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™์„ ์œ„๋ฐ˜ํ–ˆ์Šต๋‹ˆ๋‹ค"), + INTERNAL_SERVER_ERROR("CMN004", "์„œ๋ฒ„ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + UNEXPECTED_ERROR("CMN005", "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + + // Lexer ๋„๋ฉ”์ธ ์˜ค๋ฅ˜ (LEX) + UNEXPECTED_CHARACTER("LEX001", "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ฌธ์ž๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + UNCLOSED_VARIABLE("LEX002", "๋ณ€์ˆ˜๊ฐ€ ๋‹ซํžˆ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค"), + INVALID_NUMBER_FORMAT("LEX003", "์ž˜๋ชป๋œ ์ˆซ์ž ํ˜•์‹์ž…๋‹ˆ๋‹ค"), + INVALID_TOKEN_SEQUENCE("LEX004", "์ž˜๋ชป๋œ ํ† ํฐ ์‹œํ€€์Šค์ž…๋‹ˆ๋‹ค"), + TOKEN_VALUE_EMPTY_EXCEPT_EOF("LEX005", "ํ† ํฐ ๊ฐ’์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค(EOF ์ œ์™ธ)"), + LEXER_VARIABLE_NAME_EMPTY("LEX006", "๋ณ€์ˆ˜๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + NOT_OPERATOR_TYPE("LEX007", "์—ฐ์‚ฐ์ž ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค"), + NOT_NUMBER_TOKEN("LEX008", "์ˆซ์ž ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค"), + NOT_BOOLEAN_TOKEN("LEX009", "๋ถˆ๋ฆฐ ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค"), + UNEXPECTED_BOOLEAN_TOKEN_TYPE("LEX010", "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋ถˆ๋ฆฐ ํ† ํฐ ํƒ€์ž…์ž…๋‹ˆ๋‹ค"), + INVALID_IDENTIFIER_FORMAT("LEX011", "์œ ํšจํ•˜์ง€ ์•Š์€ ์‹๋ณ„์ž ํ˜•์‹์ž…๋‹ˆ๋‹ค"), + INVALID_IDENTIFIER("LEX012", "์œ ํšจํ•˜์ง€ ์•Š์€ ์‹๋ณ„์ž์ž…๋‹ˆ๋‹ค"), + INVALID_VARIABLE_NAME("LEX013", "์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช…์ž…๋‹ˆ๋‹ค"), + LEXER_UNSUPPORTED_OPERATOR("LEX014", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค"), + UNSUPPORTED_DELIMITER("LEX015", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๊ตฌ๋ถ„์ž์ž…๋‹ˆ๋‹ค"), + LEXER_INVALID_BOOLEAN_VALUE("LEX016", "์œ ํšจํ•˜์ง€ ์•Š์€ ๋ถˆ๋ฆฐ ๊ฐ’์ž…๋‹ˆ๋‹ค"), + UNRECOGNIZED_TOKEN_VALUE("LEX017", "์ธ์‹ํ•  ์ˆ˜ ์—†๋Š” ํ† ํฐ ๊ฐ’์ž…๋‹ˆ๋‹ค"), + NUMBER_TOKEN_INVALID_NUMBER("LEX018", "NUMBER ํƒ€์ž… ํ† ํฐ ๊ฐ’์ด ์œ ํšจํ•œ ์ˆซ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + IDENTIFIER_TOKEN_INVALID("LEX019", "IDENTIFIER ํƒ€์ž… ํ† ํฐ ๊ฐ’์ด ์œ ํšจํ•œ ์‹๋ณ„์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + VARIABLE_TOKEN_INVALID("LEX020", "VARIABLE ํƒ€์ž… ํ† ํฐ ๊ฐ’์ด ์œ ํšจํ•œ ๋ณ€์ˆ˜๋ช…์ด ์•„๋‹™๋‹ˆ๋‹ค"), + BOOLEAN_TOKEN_INVALID("LEX021", "๋ถˆ๋ฆฐ ํƒ€์ž… ํ† ํฐ ๊ฐ’์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + UNALLOWED_CHARACTER("LEX022", "ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๋ฌธ์ž์ž…๋‹ˆ๋‹ค"), + TOKENS_EMPTY("LEX023", "๊ฒ€์ฆํ•  ํ† ํฐ ๋ชฉ๋ก์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + TOKEN_TYPE_MISMATCH("LEX024", "ํ† ํฐ ํƒ€์ž…์ด ๊ธฐ๋Œ€์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + NUMBER_NOT_FINITE("LEX025", "์ˆซ์ž ๊ฐ’์ด ์œ ํ•œํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + NUMBER_OUT_OF_RANGE("LEX026", "์ˆซ์ž ๊ฐ’์ด ํ—ˆ์šฉ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค"), + NOT_IDENTIFIER_TOKEN("LEX027", "์‹๋ณ„์ž ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค"), + IDENTIFIER_EMPTY("LEX028", "์‹๋ณ„์ž ๊ฐ’์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + IDENTIFIER_TOO_LONG("LEX029", "์‹๋ณ„์ž ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + IDENTIFIER_INVALID_FORMAT("LEX030", "์œ ํšจํ•˜์ง€ ์•Š์€ ์‹๋ณ„์ž ํ˜•์‹์ž…๋‹ˆ๋‹ค"), + NOT_VARIABLE_TOKEN("LEX031", "๋ณ€์ˆ˜ ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค"), + VARIABLE_NAME_TOO_LONG("LEX032", "๋ณ€์ˆ˜๋ช… ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + VARIABLE_NAME_INVALID_FORMAT("LEX033", "์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช… ํ˜•์‹์ž…๋‹ˆ๋‹ค"), + OPERATOR_VALUE_EMPTY("LEX034", "์—ฐ์‚ฐ์ž ๊ฐ’์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + NOT_KEYWORD_TOKEN("LEX035", "ํ‚ค์›Œ๋“œ ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค"), + TOKEN_TOO_LONG("LEX036", "ํ† ํฐ ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + INVALID_OPERATOR_SEQUENCE("LEX037", "์œ ํšจํ•˜์ง€ ์•Š์€ ์—ฐ์‚ฐ์ž ์‹œํ€€์Šค์ž…๋‹ˆ๋‹ค"), + MULTIPLE_EOF_TOKENS("LEX038", "EOF ํ† ํฐ์ด ์—ฌ๋Ÿฌ ๊ฐœ ์กด์žฌํ•ฉ๋‹ˆ๋‹ค"), + EOF_NOT_AT_END("LEX039", "EOF ํ† ํฐ์ด ๋งˆ์ง€๋ง‰ ์œ„์น˜์— ์žˆ์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + DOLLAR_TOKEN_INVALID_VALUE("LEX040", "EOF ํƒ€์ž…์ด์ง€๋งŒ '$' ๊ฐ’์ด ์•„๋‹™๋‹ˆ๋‹ค"), + NUMBER_TOKEN_NOT_NUMERIC("LEX041", "NUMBER ํƒ€์ž…์ด์ง€๋งŒ ์ˆซ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + KEYWORD_VALUE_MISMATCH("LEX042", "ํ‚ค์›Œ๋“œ ๊ฐ’์ด ๊ธฐ๋Œ€์™€ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + INPUT_LENGTH_EXCEEDED("LEX043", "์ž…๋ ฅ ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + DISALLOWED_CHARACTER("LEX044", "ํ—ˆ์šฉ๋˜์ง€ ์•Š์€ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + FORBIDDEN_CONTROL_CHARACTER("LEX045", "๊ธˆ์ง€๋œ ์ œ์–ด ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + LINE_COUNT_EXCEEDED("LEX046", "๋ผ์ธ ์ˆ˜๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + LINE_LENGTH_EXCEEDED("LEX047", "๋ผ์ธ ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + BOM_CHARACTER_DETECTED("LEX048", "BOM ๋ฌธ์ž๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + NULL_CHARACTER_DETECTED("LEX049", "๋„ ๋ฌธ์ž๊ฐ€ ํฌํ•จ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค"), + MAX_NESTING_DEPTH_EXCEEDED("LEX050", "์ค‘์ฒฉ ๊นŠ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + EXCESSIVE_WHITESPACE_DETECTED("LEX051", "๊ณผ๋„ํ•œ ์—ฐ์† ๊ณต๋ฐฑ์ด ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + SUSPICIOUS_REPEAT_PATTERN("LEX052", "์˜์‹ฌ์Šค๋Ÿฌ์šด ๋ฐ˜๋ณต ํŒจํ„ด์ด ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + INVALID_POSITION_INDEX("LEX053", "์œ„์น˜ ์ธ๋ฑ์Šค๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + INVALID_POSITION_LINE("LEX054", "๋ผ์ธ ๋ฒˆํ˜ธ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + INVALID_POSITION_COLUMN("LEX055", "์—ด ๋ฒˆํ˜ธ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + INVALID_MAX_TOKEN_LENGTH("LEX056", "์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด ๊ฐ’์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + MAX_TOKEN_LENGTH_EXCEEDED("LEX057", "์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + MAX_TOKEN_LENGTH_INVALID("LEX058", "์ตœ๋Œ€ ํ† ํฐ ๊ธธ์ด๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + START_TIME_INVALID("LEX059", "์‹œ์ž‘ ์‹œ๊ฐ„์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + STEPS_NEGATIVE("LEX060", "์ด๋™ ๊ฑฐ๋ฆฌ๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + INVALID_LEXING_RESULT_ERROR_STATE("LEX061", "์‹คํŒจํ•œ LexingResult๋Š” ๋ฐ˜๋“œ์‹œ error ์ •๋ณด๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + NEGATIVE_ANALYSIS_DURATION("LEX062", "๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + NEGATIVE_INPUT_LENGTH("LEX063", "์ž…๋ ฅ ํ…์ŠคํŠธ ๊ธธ์ด๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + NEGATIVE_TOKEN_COUNT("LEX064", "ํ† ํฐ ๊ฐœ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + INVALID_POSITION_ORDER("LEX065", "์‹œ์ž‘ ์œ„์น˜๊ฐ€ ๋ ์œ„์น˜๋ณด๋‹ค ๋Šฆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + NEGATIVE_TOKEN_LENGTH("LEX066", "ํ† ํฐ ๊ธธ์ด๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + NEGATIVE_ADDITIONAL_LENGTH("LEX067", "์ถ”๊ฐ€ ๊ธธ์ด๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + + // Parser ๋„๋ฉ”์ธ ์˜ค๋ฅ˜ (PAR) + SYNTAX_ERROR("PAR001", "๊ตฌ๋ฌธ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + GOTO_ERROR("PAR002", "GOTO ์ƒํƒœ ์ „์ด ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค"), + LR_PARSING_ERROR("PAR003", "LR ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + GRAMMAR_CONFLICT("PAR004", "๋ฌธ๋ฒ• ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + UNEXPECTED_END_OF_INPUT("PAR005", "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์ž…๋ ฅ ์ข…๋ฃŒ์ž…๋‹ˆ๋‹ค"), + INVALID_AST_NODE("PAR006", "์ž˜๋ชป๋œ AST ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค"), + STACK_OVERFLOW("PAR007", "ํŒŒ์„œ ์Šคํƒ ์˜ค๋ฒ„ํ”Œ๋กœ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + INCOMPLETE_INPUT("PAR008", "๋ถˆ์™„์ „ํ•œ ์ž…๋ ฅ์ž…๋‹ˆ๋‹ค"), + PARSING_ERROR("PAR009", "ํŒŒ์‹ฑ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + INVALID_GRAMMAR("PAR010", "๋ฌธ๋ฒ• ์ •์˜๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + EMPTY_PRODUCTIONS("PAR011", "์ƒ์‚ฐ ๊ทœ์น™์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + EMPTY_TERMINALS("PAR012", "ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + EMPTY_NON_TERMINALS("PAR013", "๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + INVALID_START_SYMBOL("PAR014", "์‹œ์ž‘ ์‹ฌ๋ณผ์€ ๋…ผํ„ฐ๋ฏธ๋„์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + MAX_DEPTH_NON_POSITIVE("PAR015", "์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + MAX_DEPTH_EXCEEDS_LIMIT("PAR016", "์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๊ฐ€ ํ•œ๊ณ„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + CORE_ITEMS_EMPTY("PAR017", "Core ์•„์ดํ…œ ์ง‘ํ•ฉ์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + ITEMS_EMPTY("PAR018", "์•„์ดํ…œ ์ง‘ํ•ฉ์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + LALR_MERGE_CONFLICT("PAR019", "LALR ๋ณ‘ํ•ฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค"), + DOT_POSITION_NEGATIVE("PAR020", "์ ์˜ ์œ„์น˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + DOT_POSITION_EXCEEDS_LENGTH("PAR021", "์ ์˜ ์œ„์น˜๊ฐ€ ์ƒ์„ฑ ๊ทœ์น™ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + LOOKAHEAD_NOT_TERMINAL("PAR022", "์„ ํ–‰ ์‹ฌ๋ณผ์€ ํ„ฐ๋ฏธ๋„์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + ITEM_ALREADY_COMPLETE("PAR023", "์™„๋ฃŒ๋œ ์•„์ดํ…œ์˜ ์ ์€ ๋” ์ด์ƒ ์ด๋™ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + ITEM_SET_MERGE_CONFLICT("PAR024", "์•„์ดํ…œ ์ง‘ํ•ฉ ๋ณ‘ํ•ฉ์ด ๋ถˆ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค"), + STATE_ID_NEGATIVE("PAR025", "์ƒํƒœ ID๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + STATE_ITEMS_EMPTY("PAR026", "ํŒŒ์‹ฑ ์ƒํƒœ๋Š” ์ตœ์†Œ ํ•˜๋‚˜์˜ LR ์•„์ดํ…œ์„ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + ACCEPTING_MUST_BE_FINAL("PAR027", "์ˆ˜๋ฝ ์ƒํƒœ๋Š” ๋ฐ˜๋“œ์‹œ ์ตœ์ข… ์ƒํƒœ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + TRANSITION_UNAVAILABLE("PAR028", "ํ•ด๋‹น ์‹ฌ๋ณผ๋กœ ์ „์ดํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + NOT_TERMINAL_SYMBOL("PAR029", "ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์•„๋‹™๋‹ˆ๋‹ค"), + NOT_NON_TERMINAL_SYMBOL("PAR030", "๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์•„๋‹™๋‹ˆ๋‹ค"), + PRODUCTION_ID_BELOW_MIN("PAR031", "์ƒ์„ฑ ๊ทœ์น™ ID๋Š” -1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + PRODUCTION_LEFT_NOT_NON_TERMINAL("PAR032", "์ƒ์„ฑ ๊ทœ์น™์˜ ์ขŒ๋ณ€์€ ๋…ผํ„ฐ๋ฏธ๋„์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + PRODUCTION_RIGHT_EMPTY("PAR033", "์ƒ์„ฑ ๊ทœ์น™์˜ ์šฐ๋ณ€์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (์—ก์‹ค๋ก  ์ƒ์„ฑ ์ œ์™ธ)"), + PRODUCTION_POSITION_OUT_OF_RANGE("PAR034", "์œ„์น˜๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค"), + PRODUCTION_END_POSITION_NEGATIVE("PAR035", "๋ ์œ„์น˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + PRODUCTION_END_POSITION_EXCEEDS("PAR036", "๋ ์œ„์น˜๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค"), + PRODUCTION_START_POSITION_NEGATIVE("PAR037", "์‹œ์ž‘ ์œ„์น˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + PRODUCTION_START_POSITION_EXCEEDS("PAR038", "์‹œ์ž‘ ์œ„์น˜๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค"), + AST_BUILDER_VALIDATION_FAILED("PAR039", "AST ๋นŒ๋” ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + NOT_ARITHMETIC_OPERATOR("PAR040", "์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + UNSUPPORTED_ARITHMETIC_OPERATOR("PAR041", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค"), + NOT_LOGICAL_OPERATOR("PAR042", "๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + UNSUPPORTED_LOGICAL_OPERATOR("PAR043", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค"), + NOT_COMPARISON_OPERATOR("PAR044", "๋น„๊ต ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + UNSUPPORTED_COMPARISON_OPERATOR("PAR045", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋น„๊ต ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค"), + NOT_LITERAL_TOKEN("PAR046", "๋ฆฌํ„ฐ๋Ÿด ํ† ํฐ์ด ์•„๋‹™๋‹ˆ๋‹ค"), + UNSUPPORTED_LITERAL_TYPE("PAR047", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋ฆฌํ„ฐ๋Ÿด ํƒ€์ž…์ž…๋‹ˆ๋‹ค"), + BUILDER_NAME_BLANK("PAR048", "๋นŒ๋” ์ด๋ฆ„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + OPERATOR_BLANK("PAR049", "์—ฐ์‚ฐ์ž๋Š” ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + OPERATOR_TOO_LONG("PAR050", "์—ฐ์‚ฐ์ž ๊ธธ์ด๊ฐ€ ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค"), + KERNEL_DOT_POSITION_INVALID("PAR051", "์ปค๋„ ์•„์ดํ…œ์˜ ์  ์œ„์น˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค (ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™ ์ œ์™ธ)"), + START_ITEM_MUST_USE_AUGMENTED("PAR052", "์‹œ์ž‘ ์•„์ดํ…œ์€ ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™์„ ์‚ฌ์šฉํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + PRODUCTION_LENGTH_EXCEEDS_LIMIT("PAR053", "์ƒ์‚ฐ ๊ทœ์น™์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + LOOKAHEAD_SIZE_EXCEEDS_LIMIT("PAR054", "์ „๋ฐฉํƒ์ƒ‰ ์‹ฌ๋ณผ ๊ฐœ์ˆ˜๊ฐ€ ํ•œ๋„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + INPUT_BLANK("PAR055", "์ž…๋ ฅ์ด ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + ACTION_SHIFT_STATE_REQUIRED("PAR056", "Shift ์•ก์…˜์—๋Š” ์ƒํƒœ ๋ฒˆํ˜ธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"), + ACTION_REDUCE_PRODUCTION_REQUIRED("PAR057", "Reduce ์•ก์…˜์—๋Š” ์ƒ์‚ฐ ๊ทœ์น™์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"), + UNSUPPORTED_ACTION_TYPE("PAR058", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์•ก์…˜ ํƒ€์ž…์ž…๋‹ˆ๋‹ค"), + ACCEPTING_STATE_ITEMS_NOT_COMPLETE("PAR059", "์ˆ˜๋ฝ ์ƒํƒœ๋Š” ์™„์„ฑ๋œ ์•„์ดํ…œ๋“ค๋งŒ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + STATE_COUNT_EXCEEDS_LIMIT("PAR060", "์ƒํƒœ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + ITEMS_PER_STATE_EXCEEDS_LIMIT("PAR061", "์ƒํƒœ์˜ ์•„์ดํ…œ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + ACTION_TABLE_CONTAINS_NON_TERMINAL("PAR062", "์•ก์…˜ ํ…Œ์ด๋ธ”์— ๋น„ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์žˆ์Šต๋‹ˆ๋‹ค"), + GOTO_TABLE_CONTAINS_TERMINAL("PAR063", "Goto ํ…Œ์ด๋ธ”์— ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ์žˆ์Šต๋‹ˆ๋‹ค"), + TARGET_STATE_NEGATIVE("PAR064", "๋ชฉํ‘œ ์ƒํƒœ ID๊ฐ€ ์Œ์ˆ˜์ž…๋‹ˆ๋‹ค"), + TRANSITIONS_PER_STATE_EXCEEDS_LIMIT("PAR065", "์ „์ด ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + NOT_OPERATOR_SYMBOL("PAR066", "์—ฐ์‚ฐ์ž ์‹ฌ๋ณผ์ด ์•„๋‹™๋‹ˆ๋‹ค"), + INVALID_BNF_FORMAT("PAR067", "์ž˜๋ชป๋œ BNF ํ˜•์‹์ž…๋‹ˆ๋‹ค"), + PRODUCTION_COUNT_EXCEEDS_LIMIT("PAR068", "์ƒ์‚ฐ ๊ทœ์น™ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + UNKNOWN_TOKEN_TYPE("PAR069", "์•Œ ์ˆ˜ ์—†๋Š” ํ† ํฐ ํƒ€์ž…์ž…๋‹ˆ๋‹ค"), + ASSOCIATIVITY_OPERATOR_MISMATCH("PAR070", "๊ฒฐํ•ฉ์„ฑ ๊ทœ์น™์˜ ์—ฐ์‚ฐ์ž์™€ ํ† ํฐ ํƒ€์ž…์ด ์ผ์น˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + LEFT_RECURSION_DETECTED("PAR071", "์ขŒ์žฌ๊ท€๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + UNREACHABLE_NON_TERMINALS("PAR072", "๋„๋‹ฌ ๋ถˆ๊ฐ€๋Šฅํ•œ ๋…ผํ„ฐ๋ฏธ๋„๋“ค์ž…๋‹ˆ๋‹ค"), + UNDEFINED_NON_TERMINALS("PAR073", "์ •์˜๋˜์ง€ ์•Š์€ ๋…ผํ„ฐ๋ฏธ๋„๋“ค์ž…๋‹ˆ๋‹ค"), + AMBIGUOUS_GRAMMAR_RULE("PAR074", "๋ชจํ˜ธํ•œ ๋ฌธ๋ฒ• ๊ทœ์น™์ด ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + CYCLIC_GRAMMAR_REFERENCE("PAR075", "์ˆœํ™˜ ์ฐธ์กฐ๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + PRODUCTION_COUNT_BELOW_MIN("PAR076", "์ƒ์‚ฐ ๊ทœ์น™์ด ์ตœ์†Œ ๊ฐœ์ˆ˜๋ณด๋‹ค ์ ์Šต๋‹ˆ๋‹ค"), + START_SYMBOL_NOT_IN_NON_TERMINALS("PAR077", "์‹œ์ž‘ ์‹ฌ๋ณผ์ด ๋…ผํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ์— ํฌํ•จ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + TERMINALS_NON_TERMINALS_OVERLAP("PAR078", "ํ„ฐ๋ฏธ๋„๊ณผ ๋…ผํ„ฐ๋ฏธ๋„ ์ง‘ํ•ฉ์ด ๊ฒน์นฉ๋‹ˆ๋‹ค"), + DUPLICATE_PRODUCTIONS("PAR079", "์ค‘๋ณต๋œ ์ƒ์‚ฐ ๊ทœ์น™์ด ์กด์žฌํ•ฉ๋‹ˆ๋‹ค"), + PRODUCTION_ID_NEGATIVE("PAR080", "์ƒ์‚ฐ ๊ทœ์น™ ID๊ฐ€ ์Œ์ˆ˜์ž…๋‹ˆ๋‹ค"), + UNKNOWN_SYMBOL_IN_PRODUCTION("PAR081", "์•Œ ์ˆ˜ ์—†๋Š” ์‹ฌ๋ณผ์ด ์ƒ์‚ฐ ๊ทœ์น™์— ์‚ฌ์šฉ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + LALR_STATES_CANNOT_MERGE("PAR082", "์ƒํƒœ๋“ค์„ LALR๋กœ ๋ณ‘ํ•ฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + NO_STATES_TO_MERGE("PAR083", "๋ณ‘ํ•ฉํ•  ์ƒํƒœ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"), + CONFLICT_RESOLUTION_EXCEEDED("PAR084", "์ถฉ๋Œ ํ•ด๊ฒฐ์ด ์ตœ๋Œ€ ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + SHIFT_REDUCE_CONFLICT("PAR085", "Shift/Reduce ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + REDUCE_REDUCE_CONFLICT_UNRESOLVABLE("PAR086", "Reduce/Reduce ์ถฉ๋Œ์„ ํ•ด๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + NON_ASSOCIATIVE_OPERATOR_CONFLICT("PAR087", "๋น„๊ฒฐํ•ฉ ์—ฐ์‚ฐ์ž ์ถฉ๋Œ์ด ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + PRODUCTION_ID_ONLY_FOR_REDUCE("PAR088", "Reduce ์•ก์…˜๋งŒ์ด ์ƒ์‚ฐ ๊ทœ์น™ ID๋ฅผ ๊ฐ€์ง‘๋‹ˆ๋‹ค"), + FIRST_SET_NOT_CONVERGING("PAR089", "FIRST ์ง‘ํ•ฉ ๊ณ„์‚ฐ์ด ์ˆ˜๋ ดํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + FOLLOW_SET_NOT_CONVERGING("PAR090", "FOLLOW ์ง‘ํ•ฉ ๊ณ„์‚ฐ์ด ์ˆ˜๋ ดํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AUGMENTED_PRODUCTION_NOT_FOUND("PAR091", "ํ™•์žฅ ์ƒ์‚ฐ ๊ทœ์น™์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + LR_CONFLICT_DETECTED("PAR092", "Reduce/Reduce ๋˜๋Š” Shift/Reduce ์ถฉ๋Œ์ด ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + NUM_STATES_NOT_POSITIVE("PAR093", "์ƒํƒœ ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + NUM_TERMINALS_NOT_POSITIVE("PAR094", "ํ„ฐ๋ฏธ๋„ ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + NUM_NON_TERMINALS_NOT_POSITIVE("PAR095", "๋…ผํ„ฐ๋ฏธ๋„ ์ˆ˜๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + MAX_PARSING_DEPTH_NOT_POSITIVE("PAR096", "์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + MAX_PARSING_DEPTH_EXCEEDS_LIMIT("PAR097", "์ตœ๋Œ€ ํŒŒ์‹ฑ ๊นŠ์ด๊ฐ€ ํ•œ๊ณ„๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + TOKEN_COUNT_EXCEEDS_LIMIT("PAR098", "ํ† ํฐ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + MAX_STACK_SIZE_NOT_POSITIVE("PAR099", "์ตœ๋Œ€ ์Šคํƒ ํฌ๊ธฐ๋Š” ์–‘์ˆ˜์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + TOKEN_SEQUENCE_EXCEEDS_LIMIT("PAR100", "ํ† ํฐ ์‹œํ€€์Šค๊ฐ€ ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + NESTING_DEPTH_EXCEEDS_LIMIT("PAR101", "์ค‘์ฒฉ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + EXPRESSION_COMPLEXITY_EXCEEDS_LIMIT("PAR102", "ํ‘œํ˜„์‹ ๋ณต์žก๋„๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + OPERATOR_TOKEN_REQUIRED("PAR103", "์—ฐ์‚ฐ์ž ํ† ํฐ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + PRECEDENCE_NEGATIVE("PAR104", "์šฐ์„ ์ˆœ์œ„๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + UNKNOWN_ASSOCIATIVITY_SYMBOL("PAR105", "์•Œ ์ˆ˜ ์—†๋Š” ๊ฒฐํ•ฉ์„ฑ ์‹ฌ๋ณผ์ž…๋‹ˆ๋‹ค"), + PRODUCTION_ID_OUT_OF_RANGE("PAR106", "์ƒ์„ฑ ๊ทœ์น™ ID๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค"), + PRODUCTION_NOT_FOUND("PAR107", "์ƒ์„ฑ ๊ทœ์น™์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + SYMBOL_NOT_NON_TERMINAL("PAR108", "๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"), + GRAMMAR_INVALID("PAR109", "๋ฌธ๋ฒ•์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + NOT_REDUCE_ACTION("PAR110", "Reduce ์•ก์…˜์ด ์•„๋‹™๋‹ˆ๋‹ค"), + TOKEN_VALUE_EMPTY("PAR112", "ํ† ํฐ์˜ ๊ฐ’์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + ARGUMENTS_EMPTY("PAR113", "์ธ์ˆ˜ ๋ชฉ๋ก์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + ARGUMENT_INDEX_OUT_OF_RANGE("PAR114", "์ธ์ˆ˜ ์ธ๋ฑ์Šค๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค"), + UNSUPPORTED_OBJECT_TYPE("PAR115", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ํƒ€์ž…์ž…๋‹ˆ๋‹ค"), + FAILED_RESULT_MISSING_ERROR("PAR116", "์‹คํŒจํ•œ ParsingResult๋Š” ๋ฐ˜๋“œ์‹œ error ์ •๋ณด๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + DURATION_NEGATIVE("PAR117", "๋ถ„์„ ์†Œ์š” ์‹œ๊ฐ„์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + TOKEN_COUNT_NEGATIVE("PAR118", "ํ† ํฐ ๊ฐœ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + NODE_COUNT_NEGATIVE("PAR119", "๋…ธ๋“œ ๊ฐœ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + MAX_DEPTH_NEGATIVE("PAR120", "์ตœ๋Œ€ ๊นŠ์ด๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + SUCCESS_RESULT_MISSING_AST("PAR121", "์„ฑ๊ณตํ•œ ParsingResult๋Š” ๋ฐ˜๋“œ์‹œ AST๋ฅผ ํฌํ•จํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + INVALID_STATE_ID("PAR122", "์œ ํšจํ•˜์ง€ ์•Š์€ ์ƒํƒœ ID์ž…๋‹ˆ๋‹ค"), + STATE_NOT_FOUND("PAR123", "์ƒํƒœ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + TERMINAL_SYMBOL_REQUIRED("PAR124", "ํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"), + NON_TERMINAL_SYMBOL_REQUIRED("PAR125", "๋…ผํ„ฐ๋ฏธ๋„ ์‹ฌ๋ณผ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค"), + + // AST ๋„๋ฉ”์ธ ์˜ค๋ฅ˜ (AST) + AST_BUILD_ERROR("AST001", "AST ๋นŒ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + NOT_AST_NODE("AST002", "AST ๋…ธ๋“œ๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + UNSUPPORTED_AST_TYPE("AST003", "์ง€์›ํ•˜์ง€ ์•Š๋Š” AST ํƒ€์ž…์ž…๋‹ˆ๋‹ค"), + INVALID_NODE_STRUCTURE("AST004", "์ž˜๋ชป๋œ ๋…ธ๋“œ ๊ตฌ์กฐ์ž…๋‹ˆ๋‹ค"), + AST_VALIDATION_FAILED("AST005", "AST ๊ฒ€์ฆ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + AST_OPTIMIZATION_FAILED("AST006", "AST ์ตœ์ ํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + AST_TRAVERSAL_ERROR("AST007", "AST ์ˆœํšŒ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + AST_TYPE_MISMATCH("AST008", "AST ํƒ€์ž…์ด ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_SIZE_EXCEEDED("AST009", "AST ํฌ๊ธฐ๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + AST_DEPTH_EXCEEDED("AST010", "AST ๊นŠ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + INVALID_ROOT_NODE("AST011", "AST ๋ฃจํŠธ ๋…ธ๋“œ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + INVALID_REPLACEMENT_NODE("AST012", "๊ต์ฒดํ•  ๋…ธ๋“œ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_ARGUMENT_COUNT_EXCEEDED("AST013", "์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ๋Ÿ‰์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + AST_INDEX_OUT_OF_RANGE("AST014", "์ธ๋ฑ์Šค๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค"), + AST_OPERATOR_EMPTY("AST015", "์—ฐ์‚ฐ์ž๋Š” ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_UNSUPPORTED_OPERATOR("AST016", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค"), + AST_OPERATOR_NOT_COMMUTATIVE("AST017", "๊ตํ™˜๋ฒ•์น™์ด ์„ฑ๋ฆฝํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค"), + INVALID_BOOLEAN_VALUE("AST018", "์ž˜๋ชป๋œ ๋ถˆ๋ฆฐ ๊ฐ’์ž…๋‹ˆ๋‹ค"), + AST_FUNCTION_NAME_EMPTY("AST019", "ํ•จ์ˆ˜๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_INVALID_FUNCTION_NAME("AST020", "์œ ํšจํ•˜์ง€ ์•Š์€ ํ•จ์ˆ˜๋ช…์ž…๋‹ˆ๋‹ค"), + AST_ARGUMENTS_EMPTY("AST021", "์ธ์ˆ˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"), + AST_IF_NOT_SIMPLIFIABLE("AST022", "๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์—†๋Š” IF ๋…ธ๋“œ์ž…๋‹ˆ๋‹ค"), + AST_SIMPLIFICATION_UNEXPECTED_CASE("AST023", "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๋‹จ์ˆœํ™” ์ผ€์ด์Šค"), + AST_NON_FINITE_NUMBER("AST024", "์ˆซ์ž ๊ฐ’์€ ์œ ํ•œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + AST_NON_INTEGER_TO_INT("AST025", "์ •์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฐ’์„ ์ •์ˆ˜๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_NON_INTEGER_TO_LONG("AST026", "์ •์ˆ˜๊ฐ€ ์•„๋‹Œ ๊ฐ’์„ Long์œผ๋กœ ๋ณ€ํ™˜ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_DIVISION_BY_ZERO("AST027", "0์œผ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_UNSUPPORTED_UNARY_OPERATOR("AST028", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ๋‹จํ•ญ ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค"), + AST_DOUBLE_NEGATION_NOT_SIMPLIFIABLE("AST029", "์ด์ค‘ ์Œ์ˆ˜๋ฅผ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_DOUBLE_LOGICAL_NEGATION_NOT_SIMPLIFIABLE("AST030", "์ด์ค‘ ๋…ผ๋ฆฌ ๋ถ€์ •์„ ๋‹จ์ˆœํ™”ํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_VARIABLE_NAME_EMPTY("AST031", "๋ณ€์ˆ˜๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_INVALID_VARIABLE_NAME("AST032", "์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช…์ž…๋‹ˆ๋‹ค"), + AST_VARIABLE_NOT_BRACKETED("AST033", "๋ณ€์ˆ˜๋Š” ์ค‘๊ด„ํ˜ธ๋กœ ๋‘˜๋Ÿฌ์‹ธ์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + AST_INVALID_NUMBER_LITERAL("AST034", "์œ ํšจํ•˜์ง€ ์•Š์€ ์ˆซ์ž ํ˜•์‹์ž…๋‹ˆ๋‹ค"), + AST_NOT_ARITHMETIC_OPERATOR("AST035", "์‚ฐ์ˆ  ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + AST_NOT_COMPARISON_OPERATOR("AST036", "๋น„๊ต ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + AST_NOT_LOGICAL_OPERATOR("AST037", "๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž๊ฐ€ ์•„๋‹™๋‹ˆ๋‹ค"), + AST_UNSUPPORTED_MATH_FUNCTION("AST038", "์ง€์›๋˜์ง€ ์•Š๋Š” ์ˆ˜ํ•™ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค"), + AST_ARGS_MULTIPLE_CHILDREN_MISMATCH("AST039", "ArgsMultiple ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + AST_ARGS_SINGLE_CHILD_MISMATCH("AST040", "ArgsSingle ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + AST_BINARY_CHILDREN_INSUFFICIENT("AST041", "BinaryOp ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"), + AST_FUNCTION_CALL_CHILDREN_MISMATCH("AST042", "FunctionCall ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + AST_FUNCTION_CALL_FIRST_NOT_TOKEN("AST043", "FunctionCall ๋นŒ๋”์˜ ์ฒซ ๋ฒˆ์งธ ์ž์‹์€ Token์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + AST_FUNCTION_CALL_THIRD_NOT_LIST("AST044", "FunctionCall ๋นŒ๋”์˜ ์„ธ ๋ฒˆ์งธ ์ž์‹์€ List์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + AST_FUNCTION_CALL_ARGS_NOT_AST_NODE("AST045", "FunctionCall ๋นŒ๋”์˜ ์ธ์ˆ˜ ๋ชฉ๋ก์€ ASTNode์—ฌ์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + AST_FUNCTION_CALL_EMPTY_CHILDREN_MISMATCH("AST046", "FunctionCallEmpty ๋นŒ๋”์˜ ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์˜ˆ์ƒ๊ณผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค"), + AST_FUNCTION_CALL_EMPTY_FIRST_NOT_TOKEN("AST047", "FunctionCallEmpty ๋นŒ๋”์˜ ์ฒซ ๋ฒˆ์งธ ์ž์‹์ด Token์ด ์•„๋‹™๋‹ˆ๋‹ค"), + AST_FUNCTION_CALL_EMPTY_SECOND_NOT_TOKEN("AST048", "FunctionCallEmpty ๋นŒ๋”์˜ ๋‘ ๋ฒˆ์งธ ์ž์‹์ด Token์ด ์•„๋‹™๋‹ˆ๋‹ค"), + AST_FUNCTION_CALL_EMPTY_THIRD_NOT_TOKEN("AST049", "FunctionCallEmpty ๋นŒ๋”์˜ ์„ธ ๋ฒˆ์งธ ์ž์‹์ด Token์ด ์•„๋‹™๋‹ˆ๋‹ค"), + AST_IDENTITY_CHILDREN_EMPTY("AST050", "Identity ๋นŒ๋” ์ž์‹์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค"), + AST_IF_CHILDREN_MISMATCH("AST051", "If ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_NUMBER_CHILDREN_MISMATCH("AST052", "Number ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_PARENTHESIZED_CHILDREN_MISMATCH("AST053", "Parenthesized ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_PARENTHESIZED_SECOND_NOT_AST("AST054", "Parenthesized ๋นŒ๋” ๋‘ ๋ฒˆ์งธ ์ž์‹์ด ASTNode ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค"), + AST_START_CHILDREN_MISMATCH("AST055", "Start ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_UNARY_CHILDREN_INSUFFICIENT("AST056", "UnaryOp ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ๋ถ€์กฑํ•ฉ๋‹ˆ๋‹ค"), + AST_VARIABLE_CHILDREN_MISMATCH("AST057", "Variable ๋นŒ๋” ์ž์‹ ๊ฐœ์ˆ˜๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_VARIABLE_FIRST_NOT_TOKEN("AST058", "Variable ๋นŒ๋” ์ฒซ ๋ฒˆ์งธ ์ž์‹์ด Token ํƒ€์ž…์ด ์•„๋‹™๋‹ˆ๋‹ค"), + AST_NUMBER_IS_NAN("AST059", "์ˆซ์ž ๊ฐ’์€ NaN์ด ๋  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_NUMBER_TOO_SMALL("AST060", "์ˆซ์ž ๊ฐ’์ด ์ตœ์†Œ๊ฐ’ ๋ฏธ๋งŒ์ž…๋‹ˆ๋‹ค"), + AST_NUMBER_TOO_LARGE("AST061", "์ˆซ์ž ๊ฐ’์ด ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_VARIABLE_NAME_TOO_LONG("AST062", "๋ณ€์ˆ˜๋ช…์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_VARIABLE_RESERVED_WORD("AST063", "์˜ˆ์•ฝ์–ด๋Š” ๋ณ€์ˆ˜๋ช…์œผ๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_UNSUPPORTED_BINARY_OPERATOR("AST064", "์ง€์›๋˜์ง€ ์•Š๋Š” ์ดํ•ญ ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค"), + AST_MODULO_BY_ZERO("AST065", "0์œผ๋กœ ๋‚˜๋ˆˆ ๋‚˜๋จธ์ง€๋ฅผ ๊ตฌํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + AST_ZERO_POWER_ZERO_UNDEFINED("AST066", "0^0์€ ์ •์˜๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_LOGICAL_INCOMPATIBLE_OPERAND("AST067", "๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž๋Š” ๋…ผ๋ฆฌ์ ์œผ๋กœ ํ˜ธํ™˜๋˜๋Š” ํ”ผ์—ฐ์‚ฐ์ž๋งŒ ํ—ˆ์šฉํ•ฉ๋‹ˆ๋‹ค"), + AST_FUNCTION_NAME_TOO_LONG("AST068", "ํ•จ์ˆ˜๋ช…์ด ์ตœ๋Œ€ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_FUNCTION_ARGUMENTS_EXCEEDED("AST069", "ํ•จ์ˆ˜ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_FUNCTION_ARGUMENT_COUNT_MISMATCH("AST070", "ํ•จ์ˆ˜ ์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์š”๊ตฌ์‚ฌํ•ญ๊ณผ ์ผ์น˜ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_IF_TOTAL_DEPTH_EXCEEDED("AST071", "์กฐ๊ฑด๋ฌธ์˜ ์ด ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_ARGUMENTS_EXCEEDED("AST072", "์ธ์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_ARGUMENTS_DUPLICATED("AST073", "์ค‘๋ณต๋œ ์ธ์ˆ˜๊ฐ€ ๋ฐœ๊ฒฌ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + AST_NODE_SIZE_EXCEEDED("AST074", "๋…ธ๋“œ ํฌ๊ธฐ๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_NODE_DEPTH_EXCEEDED("AST075", "๋…ธ๋“œ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_NODE_VARIABLES_EXCEEDED("AST076", "๋…ธ๋“œ์˜ ๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_TREE_DEPTH_NEGATIVE("AST077", "ํŠธ๋ฆฌ ๊นŠ์ด๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + AST_TREE_DEPTH_TOO_LARGE("AST078", "ํŠธ๋ฆฌ ๊นŠ์ด๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + AST_RUNTIME_RULE_NOT_SUPPORTED("AST079", "ํ˜„์žฌ ๋ฒ„์ „์—์„œ๋Š” ๋Ÿฐํƒ€์ž„ ๊ทœ์น™ ์ถ”๊ฐ€๋ฅผ ์ง€์›ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + AST_NODE_SIZE_NEGATIVE("AST080", "๋…ธ๋“œ ํฌ๊ธฐ๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + AST_NODE_SIZE_TOO_LARGE("AST081", "๋…ธ๋“œ ํฌ๊ธฐ๊ฐ€ ์ตœ๋Œ€๊ฐ’์„ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค"), + + // Evaluator ๋„๋ฉ”์ธ ์˜ค๋ฅ˜ (EVA) + EVALUATION_ERROR("EVA001", "ํ‘œํ˜„์‹ ํ‰๊ฐ€ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + DIVISION_BY_ZERO("EVA002", "0์œผ๋กœ ๋‚˜๋ˆŒ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + UNDEFINED_VARIABLE("EVA003", "์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜์ž…๋‹ˆ๋‹ค"), + UNSUPPORTED_OPERATOR("EVA004", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์—ฐ์‚ฐ์ž์ž…๋‹ˆ๋‹ค"), + UNSUPPORTED_FUNCTION("EVA005", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค"), + WRONG_ARGUMENT_COUNT("EVA006", "์ž˜๋ชป๋œ ์ธ์ˆ˜ ๊ฐœ์ˆ˜์ž…๋‹ˆ๋‹ค"), + UNSUPPORTED_TYPE("EVA007", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ํƒ€์ž…์ž…๋‹ˆ๋‹ค"), + NUMBER_CONVERSION_ERROR("EVA008", "์ˆซ์ž ๋ณ€ํ™˜ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + MATH_ERROR("EVA009", "์ˆ˜ํ•™ ์—ฐ์‚ฐ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + TYPE_COMPATIBILITY_ERROR("EVA010", "ํƒ€์ž… ํ˜ธํ™˜์„ฑ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + TYPE_EVALUATOR_ERROR("EVA011", "ํƒ€์ž… ํ‰๊ฐ€ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + TYPE_ARGUMENT_ERROR("EVA012", "ํƒ€์ž… ์ธ์ˆ˜ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + TYPE_CAST_ERROR("EVA013", "ํƒ€์ž… ์บ์ŠคํŒ… ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + TYPE_LOOKUP_ERROR("EVA014", "ํƒ€์ž… ์กฐํšŒ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + TYPE_NULL_REFERENCE_ERROR("EVA015", "ํƒ€์ž… null ์ฐธ์กฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + TYPE_UNSUPPORTED_ERROR("EVA016", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ํƒ€์ž… ์—ฐ์‚ฐ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + TYPE_RUNTIME_ERROR("EVA017", "ํƒ€์ž… ๋Ÿฐํƒ€์ž„ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + EVALUATION_FAILED("EVA018", "ํ‘œํ˜„์‹ ํ‰๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + EVALUATION_TIMEOUT("EVA019", "ํ‘œํ˜„์‹ ํ‰๊ฐ€ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + EVALUATION_DEPTH_EXCEEDED("EVA020", "ํ‰๊ฐ€ ๊นŠ์ด๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + EVALUATION_COMPLEXITY_EXCEEDED("EVA021", "ํ‰๊ฐ€ ๋ณต์žก๋„๊ฐ€ ์ œํ•œ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + INVALID_EVALUATION_CONTEXT("EVA022", "์œ ํšจํ•˜์ง€ ์•Š์€ ํ‰๊ฐ€ ์ปจํ…์ŠคํŠธ์ž…๋‹ˆ๋‹ค"), + EVALUATION_SECURITY_VIOLATION("EVA023", "ํ‰๊ฐ€ ๋ณด์•ˆ ๊ทœ์น™์„ ์œ„๋ฐ˜ํ–ˆ์Šต๋‹ˆ๋‹ค"), + VARIABLE_NAME_INVALID("EVA024", "์œ ํšจํ•˜์ง€ ์•Š์€ ๋ณ€์ˆ˜๋ช…์ž…๋‹ˆ๋‹ค"), + FUNCTION_EXECUTION_FAILED("EVA025", "ํ•จ์ˆ˜ ์‹คํ–‰์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + OPERATOR_EVALUATION_FAILED("EVA026", "์—ฐ์‚ฐ์ž ํ‰๊ฐ€์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + TYPE_CONVERSION_FAILED("EVA027", "ํƒ€์ž… ๋ณ€ํ™˜์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + BUILTIN_FUNCTION_ERROR("EVA028", "๋‚ด์žฅ ํ•จ์ˆ˜ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + EVALUATION_RESULT_INVALID("EVA029", "ํ‰๊ฐ€ ๊ฒฐ๊ณผ๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + EVALUATION_MEMORY_EXCEEDED("EVA030", "ํ‰๊ฐ€ ์ค‘ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + EVALUATION_STACK_OVERFLOW("EVA031", "ํ‰๊ฐ€ ์ค‘ ์Šคํƒ ์˜ค๋ฒ„ํ”Œ๋กœ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + EVALUATION_INFINITE_LOOP("EVA032", "ํ‰๊ฐ€ ์ค‘ ๋ฌดํ•œ ๋ฃจํ”„๊ฐ€ ๊ฐ์ง€๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + VARIABLE_VALUE_INVALID("EVA033", "๋ณ€์ˆ˜ ๊ฐ’์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + FUNCTION_ARGUMENT_INVALID("EVA034", "ํ•จ์ˆ˜ ์ธ์ˆ˜๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + OPERATOR_OPERAND_INVALID("EVA035", "์—ฐ์‚ฐ์ž ํ”ผ์—ฐ์‚ฐ์ž๊ฐ€ ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + EVALUATION_CANCELLED("EVA036", "ํ‰๊ฐ€๊ฐ€ ์ทจ์†Œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + EVALUATION_INTERRUPTED("EVA037", "ํ‰๊ฐ€๊ฐ€ ์ค‘๋‹จ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + + // Calculator ๋„๋ฉ”์ธ ์˜ค๋ฅ˜ (CAL) + EMPTY_FORMULA("CAL001", "์ˆ˜์‹์ด ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + FORMULA_TOO_LONG("CAL002", "์ˆ˜์‹์ด ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค"), + EMPTY_STEPS("CAL003", "๊ณ„์‚ฐ ๋‹จ๊ณ„๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค"), + TOO_MANY_STEPS("CAL004", "๊ณ„์‚ฐ ๋‹จ๊ณ„๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์Šต๋‹ˆ๋‹ค"), + TOO_MANY_VARIABLES("CAL005", "๋ณ€์ˆ˜๊ฐ€ ๋„ˆ๋ฌด ๋งŽ์Šต๋‹ˆ๋‹ค"), + MISSING_VARIABLES("CAL006", "ํ•„์ˆ˜ ๋ณ€์ˆ˜๊ฐ€ ๋ˆ„๋ฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค"), + STEP_EXECUTION_ERROR("CAL007", "๋‹จ๊ณ„ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + FORMULA_VALIDATION_ERROR("CAL008", "์ˆ˜์‹ ๊ฒ€์ฆ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + VARIABLE_EXTRACTION_ERROR("CAL009", "๋ณ€์ˆ˜ ์ถ”์ถœ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + HEALTH_CHECK_FAILED("CAL010", "ํ•ผ์Šค ์ฒดํฌ์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + SERIALIZATION_FAILED("CAL011", "์—ญ์ง๋ ฌํ™”์— ์‹คํŒจํ–ˆ์Šต๋‹ˆ๋‹ค"), + PERFORMANCE_WARNING("CAL012", "์„ฑ๋Šฅ ๊ฒฝ๊ณ ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + VALIDATION_EXCEPTION("CAL013", "๊ณ„์‚ฐ ์œ ํšจ์„ฑ ๊ฒ€์ฆ ์ค‘ ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + SESSION_ID_EMPTY("CAL014", "์„ธ์…˜ ID๋Š” ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + CALCULATION_HISTORY_TOO_LARGE("CAL015", "๊ณ„์‚ฐ ์ด๋ ฅ์ด ์ตœ๋Œ€ ํฌ๊ธฐ๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + VARIABLE_NAME_EMPTY("CAL016", "๋ณ€์ˆ˜ ์ด๋ฆ„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + USER_ID_EMPTY("CAL017", "์‚ฌ์šฉ์ž ID๋Š” ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + EMPTY_EXPRESSIONS("CAL018", "์ˆ˜์‹ ๋ชฉ๋ก์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + STEP_TIMEOUT_EXCEEDED("CAL019", "๊ณ„์‚ฐ ๋‹จ๊ณ„๊ฐ€ ์ „์ฒด ํƒ€์ž„์•„์›ƒ์„ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + REQUEST_LIST_EMPTY("CAL020", "๊ณ„์‚ฐ ์š”์ฒญ ๋ชฉ๋ก์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + INVALID_CONCURRENCY_LEVEL("CAL021", "๋™์‹œ์„ฑ ์ˆ˜์ค€์€ 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + INVALID_BUFFER_SIZE("CAL022", "๋ฒ„ํผ ํฌ๊ธฐ๋Š” 0๋ณด๋‹ค ์ปค์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + MAX_RETRY_EXCEEDED("CAL023", "์ตœ๋Œ€ ์žฌ์‹œ๋„ ํšŸ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + INVALID_AST_NODE_TYPE("CAL024", "์œ ํšจํ•˜์ง€ ์•Š์€ AST ๋…ธ๋“œ ํƒ€์ž…์ž…๋‹ˆ๋‹ค"), + OPTION_KEY_EMPTY("CAL025", "์˜ต์…˜ ํ‚ค๋Š” ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + EXECUTION_TIME_NEGATIVE("CAL026", "์‹คํ–‰ ์‹œ๊ฐ„์€ 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + MERGE_RESULTS_EMPTY("CAL027", "๋ณ‘ํ•ฉํ•  ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค"), + STEP_NAME_EMPTY("CAL028", "๋‹จ๊ณ„ ์ด๋ฆ„์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + STEP_NAME_TOO_LONG("CAL029", "๋‹จ๊ณ„ ์ด๋ฆ„์ด ๋„ˆ๋ฌด ๊น๋‹ˆ๋‹ค"), + RESULT_VARIABLE_NAME_EMPTY("CAL030", "๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช…์€ ๋น„์–ด์žˆ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค"), + RESULT_VARIABLE_NAME_INVALID("CAL031", "๊ฒฐ๊ณผ ๋ณ€์ˆ˜๋ช…์ด ์œ ํšจํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค"), + STEPS_EMPTY("CAL032", "๊ณ„์‚ฐ ๋‹จ๊ณ„๋Š” ์ตœ์†Œ 1๊ฐœ ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค"), + STEPS_TOO_MANY("CAL033", "๊ณ„์‚ฐ ๋‹จ๊ณ„๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + VARIABLES_TOO_MANY("CAL034", "๋ณ€์ˆ˜ ๊ฐœ์ˆ˜๊ฐ€ ์ตœ๋Œ€ ํ—ˆ์šฉ ๊ฐœ์ˆ˜๋ฅผ ์ดˆ๊ณผํ–ˆ์Šต๋‹ˆ๋‹ค"), + STEP_FORMULA_EMPTY("CAL035", "๋‹จ๊ณ„์˜ ์ˆ˜์‹์ด ๋น„์–ด ์žˆ์Šต๋‹ˆ๋‹ค"), + INDEX_OUT_OF_RANGE_INCLUSIVE("CAL036", "์ธ๋ฑ์Šค๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค(์ƒํ•œ ํฌํ•จ)"), + INDEX_OUT_OF_RANGE_EXCLUSIVE("CAL037", "์ธ๋ฑ์Šค๊ฐ€ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚ฌ์Šต๋‹ˆ๋‹ค(์ƒํ•œ ์ œ์™ธ)"), + MIN_STEPS_REQUIRED("CAL038", "์ตœ์†Œ ๋‹จ๊ณ„ ๊ฐœ์ˆ˜ ์š”๊ฑด์„ ์ถฉ์กฑํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค"), + + // Expresser ๋„๋ฉ”์ธ ์˜ค๋ฅ˜ (EXP) + FORMATTING_ERROR("EXP001", "ํฌ๋งทํŒ… ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + INVALID_FORMAT_OPTION("EXP002", "์ž˜๋ชป๋œ ํฌ๋งท ์˜ต์…˜์ž…๋‹ˆ๋‹ค"), + OUTPUT_GENERATION_ERROR("EXP003", "์ถœ๋ ฅ ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค"), + INVALID_INPUT("EXP004", "์ž˜๋ชป๋œ ์ž…๋ ฅ์ž…๋‹ˆ๋‹ค"), + UNSUPPORTED_STYLE("EXP005", "์ง€์›ํ•˜์ง€ ์•Š๋Š” ์Šคํƒ€์ผ์ž…๋‹ˆ๋‹ค"), + INVALID_NODE_TYPE("EXP006", "์ž˜๋ชป๋œ ๋…ธ๋“œ ํƒ€์ž…์ž…๋‹ˆ๋‹ค"), + + // Annotation ๋„๋ฉ”์ธ ์˜ค๋ฅ˜ (ANT) + MISSING_ENTITY_ANNOTATION("ANT001", "์—”ํ‹ฐํ‹ฐ ์–ด๋…ธํ…Œ์ด์…˜ ๋ˆ„๋ฝ"), + ENTITY_CONTRACT_NOT_IMPLEMENTED("ANT002", "EntityCont,ract ๋ฏธ๊ตฌํ˜„"), + INVALID_AGGREGATE_ROOT("ANT003", "์œ ํšจํ•˜์ง€ ์•Š์€ Aggregate Root"); + + /** + * ์˜ค๋ฅ˜ ์ฝ”๋“œ์˜ ๋„๋ฉ”์ธ ์ ‘๋‘์‚ฌ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ์ ‘๋‘์‚ฌ (์˜ˆ: LEX, PAR, AST) + */ + fun getDomainPrefix(): String = code.take(3) + + /** + * ์˜ค๋ฅ˜ ์ฝ”๋“œ์˜ ์ˆซ์ž ๋ถ€๋ถ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์˜ค๋ฅ˜ ๋ฒˆํ˜ธ (์˜ˆ: 001, 002) + */ + fun getErrorNumber(): String = code.drop(3) + + /** + * ์˜ค๋ฅ˜ ์ฝ”๋“œ๊ฐ€ ํŠน์ • ๋„๋ฉ”์ธ์— ์†ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param domainPrefix ํ™•์ธํ•  ๋„๋ฉ”์ธ ์ ‘๋‘์‚ฌ + * @return ํ•ด๋‹น ๋„๋ฉ”์ธ์— ์†ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun belongsToDomain(domainPrefix: String): Boolean = getDomainPrefix() == domainPrefix.uppercase() +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/GlobalExceptionHandler.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/GlobalExceptionHandler.kt new file mode 100644 index 00000000..5dc4c38c --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/GlobalExceptionHandler.kt @@ -0,0 +1,204 @@ +package hs.kr.entrydsm.global.exception + +import hs.kr.entrydsm.global.constants.ErrorCodes + +/** + * ์‹œ์Šคํ…œ ์ „์—ญ ์˜ˆ์™ธ ์ฒ˜๋ฆฌ๋ฅผ ๋‹ด๋‹นํ•˜๋Š” ํ•ธ๋“ค๋Ÿฌ์ž…๋‹ˆ๋‹ค. + * + * ๋ชจ๋“  ํ‘œ์ค€ ์˜ˆ์™ธ๋ฅผ ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋กœ ๋ณ€ํ™˜ํ•˜์—ฌ ์ผ๊ด€๋œ ์—๋Ÿฌ ์ฒ˜๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * ํ•˜๋“œ์ฝ”๋”ฉ๋œ ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ๋Œ€์‹  ์ค‘์•™ํ™”๋œ ์—๋Ÿฌ ์ฝ”๋“œ๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.07.28 + */ +object GlobalExceptionHandler { + + // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ƒ์ˆ˜ + const val MSG_INVALID_NUMBER_FORMAT = "์ˆซ์ž ํ˜•์‹์ด ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค" + const val MSG_INVALID_ARGUMENT = "์ž˜๋ชป๋œ ์ธ์ˆ˜์ž…๋‹ˆ๋‹ค" + const val MSG_ILLEGAL_STATE = "์ž˜๋ชป๋œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค" + const val MSG_NULL_POINTER = "null ๊ฐ’์— ์ ‘๊ทผํ–ˆ์Šต๋‹ˆ๋‹ค" + const val MSG_ARITHMETIC_ERROR = "์‚ฐ์ˆ  ์—ฐ์‚ฐ ์˜ค๋ฅ˜" + const val MSG_TYPE_MISMATCH = "ํƒ€์ž… ๋ถˆ์ผ์น˜" + const val MSG_INDEX_OUT_OF_BOUNDS = "์ธ๋ฑ์Šค ๋ฒ”์œ„ ์ดˆ๊ณผ" + const val MSG_STACK_OVERFLOW = "์Šคํƒ ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ" + const val MSG_UNKNOWN_ERROR = "์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค" + + // ๊ธฐํƒ€ ์ƒ์ˆ˜ + const val CONTEXT_UNKNOWN = "Unknown" + const val DEFAULT_ERROR_MESSAGE = "Unknown error" + const val DEFAULT_CAUSE_MESSAGE = "Unknown cause" + const val ZERO_KEYWORD = "zero" + + // ๋งต ํ‚ค ์ƒ์ˆ˜ + const val KEY_ERROR_CODE = "errorCode" + const val KEY_MESSAGE = "message" + const val KEY_TYPE = "type" + const val KEY_DOMAIN = "domain" + const val KEY_TIMESTAMP = "timestamp" + const val KEY_CONTEXT = "context" + const val KEY_ROOT_CAUSE = "rootCause" + const val KEY_INPUT = "input" + + /** + * ํ‘œ์ค€ ์˜ˆ์™ธ๋ฅผ ๋„๋ฉ”์ธ ์˜ˆ์™ธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun handleException(throwable: Throwable, context: String = CONTEXT_UNKNOWN): DomainException { + return when (throwable) { + is DomainException -> throwable + is NumberFormatException -> createNumberFormatException(throwable, context) + is IllegalArgumentException -> createIllegalArgumentException(throwable, context) + is IllegalStateException -> createIllegalStateException(throwable, context) + is NullPointerException -> createNullPointerException(throwable, context) + is ArithmeticException -> createArithmeticException(throwable, context) + is ClassCastException -> createTypeMismatchException(throwable, context) + is IndexOutOfBoundsException -> createIndexOutOfBoundsException(throwable, context) + is StackOverflowError -> createStackOverflowException(throwable, context) + else -> createUnknownException(throwable, context) + } + } + + /** + * ์˜ˆ์™ธ ์ •๋ณด๋ฅผ ๋งต์œผ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun mapExceptionToInfo(throwable: Throwable): Map { + val domainException = when (throwable) { + is DomainException -> throwable + else -> handleException(throwable) + } + + return mapOf( + KEY_ERROR_CODE to domainException.errorCode.code, + KEY_MESSAGE to (domainException.message ?: DEFAULT_ERROR_MESSAGE), + KEY_TYPE to domainException.javaClass.simpleName, + KEY_DOMAIN to domainException.errorCode.code.substringBefore("0"), + KEY_TIMESTAMP to System.currentTimeMillis(), + KEY_CONTEXT to domainException.context, + KEY_ROOT_CAUSE to (getRootCause(throwable).message ?: DEFAULT_CAUSE_MESSAGE) + ) + } + + // Private helper methods + + private fun createNumberFormatException( + throwable: NumberFormatException, + context: String + ): DomainException { + return DomainException( + errorCode = ErrorCodes.Lexer.INVALID_NUMBER_FORMAT, + message = "$MSG_INVALID_NUMBER_FORMAT: ${throwable.message}", + cause = throwable, + context = mapOf(KEY_CONTEXT to context, KEY_INPUT to (throwable.message ?: "")) + ) + } + + private fun createIllegalArgumentException( + throwable: IllegalArgumentException, + context: String + ): DomainException { + return DomainException( + errorCode = ErrorCodes.Common.INVALID_ARGUMENT, + message = "$MSG_INVALID_ARGUMENT: ${throwable.message}", + cause = throwable, + context = mapOf(KEY_CONTEXT to context) + ) + } + + private fun createIllegalStateException( + throwable: IllegalStateException, + context: String + ): DomainException { + return DomainException( + errorCode = ErrorCodes.Common.ILLEGAL_STATE, + message = "$MSG_ILLEGAL_STATE: ${throwable.message}", + cause = throwable, + context = mapOf(KEY_CONTEXT to context) + ) + } + + private fun createNullPointerException( + throwable: NullPointerException, + context: String + ): DomainException { + return DomainException( + errorCode = ErrorCodes.Common.NULL_POINTER, + message = "$MSG_NULL_POINTER: ${throwable.message ?: ""}", + cause = throwable, + context = mapOf(KEY_CONTEXT to context) + ) + } + + private fun createArithmeticException( + throwable: ArithmeticException, + context: String + ): DomainException { + val errorCode = if (throwable.message?.contains(ZERO_KEYWORD) == true) { + ErrorCodes.Evaluator.DIVISION_BY_ZERO + } else { + ErrorCodes.Evaluator.ARITHMETIC_OVERFLOW + } + + return DomainException( + errorCode = errorCode, + message = "$MSG_ARITHMETIC_ERROR: ${throwable.message}", + cause = throwable, + context = mapOf(KEY_CONTEXT to context) + ) + } + + private fun createTypeMismatchException( + throwable: ClassCastException, + context: String + ): DomainException { + return DomainException( + errorCode = ErrorCodes.Evaluator.TYPE_MISMATCH, + message = "$MSG_TYPE_MISMATCH: ${throwable.message}", + cause = throwable, + context = mapOf(KEY_CONTEXT to context) + ) + } + + private fun createIndexOutOfBoundsException( + throwable: IndexOutOfBoundsException, + context: String + ): DomainException { + return DomainException( + errorCode = ErrorCodes.Parser.UNEXPECTED_EOF, + message = "$MSG_INDEX_OUT_OF_BOUNDS: ${throwable.message}", + cause = throwable, + context = mapOf(KEY_CONTEXT to context) + ) + } + + private fun createStackOverflowException( + throwable: StackOverflowError, + context: String + ): DomainException { + return DomainException( + errorCode = ErrorCodes.AST.MAX_DEPTH_EXCEEDED, + message = "$MSG_STACK_OVERFLOW: ${throwable.message ?: ""}", + cause = throwable, + context = mapOf(KEY_CONTEXT to context) + ) + } + + private fun createUnknownException( + throwable: Throwable, + context: String + ): DomainException { + return DomainException( + errorCode = ErrorCodes.Common.UNKNOWN_ERROR, + message = "$MSG_UNKNOWN_ERROR: ${throwable.message}", + cause = throwable, + context = mapOf(KEY_CONTEXT to context, KEY_TYPE to throwable.javaClass.simpleName) + ) + } + + private fun getRootCause(throwable: Throwable): Throwable { + var cause = throwable + while (cause.cause != null && cause.cause != cause) { + cause = cause.cause!! + } + return cause + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/ValidationException.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/ValidationException.kt new file mode 100644 index 00000000..f62a6be5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/exception/ValidationException.kt @@ -0,0 +1,94 @@ +package hs.kr.entrydsm.global.exception + +/** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด๋‚˜ ๊ฐ’์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ์‹คํŒจ ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ์ž…๋‹ˆ๋‹ค. + * + * ์ž…๋ ฅ ๋ฐ์ดํ„ฐ ๊ฒ€์ฆ, ๋น„์ฆˆ๋‹ˆ์Šค ๊ทœ์น™ ๊ฒ€์ฆ, ์ œ์•ฝ์กฐ๊ฑด ์œ„๋ฐ˜ ๋“ฑ ๋„๋ฉ”์ธ์—์„œ ์ •์˜๋œ + * ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๊ทœ์น™์„ ์œ„๋ฐ˜ํ–ˆ์„ ๋•Œ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ฆ ์‹คํŒจํ•œ ํ•„๋“œ ์ •๋ณด์™€ + * ์ƒ์„ธํ•œ ์˜ค๋ฅ˜ ๋‚ด์šฉ์„ ํฌํ•จํ•˜์—ฌ ํด๋ผ์ด์–ธํŠธ์—๊ฒŒ ๋ช…ํ™•ํ•œ ํ”ผ๋“œ๋ฐฑ์„ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @property field ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ํ•„๋“œ๋ช… (์„ ํƒ์‚ฌํ•ญ) + * @property value ๊ฒ€์ฆ์— ์‹คํŒจํ•œ ๊ฐ’ (์„ ํƒ์‚ฌํ•ญ) + * @property constraint ์œ„๋ฐ˜๋œ ์ œ์•ฝ์กฐ๊ฑด ์„ค๋ช… (์„ ํƒ์‚ฌํ•ญ) + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +class ValidationException( + errorCode: ErrorCode = ErrorCode.VALIDATION_FAILED, + val field: String? = null, + val value: Any? = null, + val constraint: String? = null, + message: String = buildValidationMessage(errorCode, field, value, constraint), + cause: Throwable? = null +) : DomainException(errorCode, message, cause) { + + companion object { + /** + * ๊ฒ€์ฆ ์‹คํŒจ ๋ฉ”์‹œ์ง€๋ฅผ ๊ตฌ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param errorCode ์˜ค๋ฅ˜ ์ฝ”๋“œ + * @param field ๊ฒ€์ฆ ์‹คํŒจ ํ•„๋“œ๋ช… + * @param value ๊ฒ€์ฆ ์‹คํŒจ ๊ฐ’ + * @param constraint ์œ„๋ฐ˜๋œ ์ œ์•ฝ์กฐ๊ฑด + * @return ๊ตฌ์„ฑ๋œ ๋ฉ”์‹œ์ง€ + */ + private fun buildValidationMessage( + errorCode: ErrorCode, + field: String?, + value: Any?, + constraint: String? + ): String { + val baseMessage = errorCode.description + val details = mutableListOf() + + field?.let { details.add("ํ•„๋“œ: $it") } + value?.let { details.add("๊ฐ’: $it") } + constraint?.let { details.add("์ œ์•ฝ์กฐ๊ฑด: $it") } + + return if (details.isNotEmpty()) { + "$baseMessage (${details.joinToString(", ")})" + } else { + baseMessage + } + } + } + + /** + * ๊ฒ€์ฆ ์‹คํŒจ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•„๋“œ, ๊ฐ’, ์ œ์•ฝ์กฐ๊ฑด ์ •๋ณด๊ฐ€ ํฌํ•จ๋œ ๋งต + */ + fun getValidationInfo(): Map = mapOf( + "field" to field, + "value" to value, + "constraint" to constraint + ).filterValues { it != null } + + /** + * ์ „์ฒด ์˜ค๋ฅ˜ ์ •๋ณด๋ฅผ ๊ตฌ์กฐํ™”๋œ ๋งต์œผ๋กœ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ์˜ค๋ฅ˜ ์ •๋ณด์™€ ๊ฒ€์ฆ ์ •๋ณด๊ฐ€ ๊ฒฐํ•ฉ๋œ ๋งต + */ + fun getFullErrorInfo(): Map { + val baseInfo = super.toErrorInfo().toMutableMap() + val validationInfo = getValidationInfo() + + validationInfo.forEach { (key, value) -> + baseInfo[key] = value.toString() + } + + return baseInfo + } + + override fun toString(): String { + val validationDetails = getValidationInfo() + return if (validationDetails.isNotEmpty()) { + "${super.toString()}, validation=${validationDetails}" + } else { + super.toString() + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/extensions/FirstFollowSetsExtensions.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/extensions/FirstFollowSetsExtensions.kt new file mode 100644 index 00000000..cec0b427 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/extensions/FirstFollowSetsExtensions.kt @@ -0,0 +1,54 @@ +package hs.kr.entrydsm.global.extensions + +import hs.kr.entrydsm.domain.parser.values.FirstFollowSets + +/** + * FirstFollowSets ํด๋ž˜์Šค๋ฅผ ์œ„ํ•œ ํ†ต๊ณ„ ๋ฐ ๋””๋ฒ„๊น… ํ™•์žฅ ํ•จ์ˆ˜๋“ค์ž…๋‹ˆ๋‹ค. + * + * ์ด ํ™•์žฅ ํ•จ์ˆ˜๋“ค์€ ๊ฐœ๋ฐœ ๋„๊ตฌ ๋ฐ ๋””๋ฒ„๊น… ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋ฉฐ, + * ํ•ต์‹ฌ ๋„๋ฉ”์ธ ๋กœ์ง๊ณผ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ช…ํ™•ํžˆ ํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.13 + */ + +/** + * ๊ณ„์‚ฐ๋œ FIRST ์ง‘ํ•ฉ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return FIRST ์ง‘ํ•ฉ ํ†ต๊ณ„ ๋งต + */ +fun FirstFollowSets.getFirstStats(): Map { + val firstSets = this.javaClass.getDeclaredField(Field.FIRST_SETS).apply { isAccessible = true }.get(this) as Map<*, *> + + return mapOf( + "totalSymbols" to firstSets.size, + "nonEmptyFirstSets" to firstSets.values.count { (it as Set<*>).isNotEmpty() }, + "averageFirstSetSize" to if (firstSets.isNotEmpty()) { + firstSets.values.map { (it as Set<*>).size }.average() + } else 0.0, + "maxFirstSetSize" to (firstSets.values.maxOfOrNull { (it as Set<*>).size } ?: 0) + ) +} + +/** + * ๊ณ„์‚ฐ๋œ FOLLOW ์ง‘ํ•ฉ์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return FOLLOW ์ง‘ํ•ฉ ํ†ต๊ณ„ ๋งต + */ +fun FirstFollowSets.getFollowStats(): Map { + val followSets = this.javaClass.getDeclaredField(Field.FOLLOW_SETS).apply { isAccessible = true }.get(this) as Map<*, *> + + return mapOf( + "totalSymbols" to followSets.size, + "nonEmptyFollowSets" to followSets.values.count { (it as Set<*>).isNotEmpty() }, + "averageFollowSetSize" to if (followSets.isNotEmpty()) { + followSets.values.map { (it as Set<*>).size }.average() + } else 0.0, + "maxFollowSetSize" to (followSets.values.maxOfOrNull { (it as Set<*>).size } ?: 0) + ) +} + +object Field { + const val FIRST_SETS = "firstSets" + const val FOLLOW_SETS = "followSets" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/extensions/ParsingTableExtensions.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/extensions/ParsingTableExtensions.kt new file mode 100644 index 00000000..8839ea24 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/extensions/ParsingTableExtensions.kt @@ -0,0 +1,158 @@ +package hs.kr.entrydsm.global.extensions + +import hs.kr.entrydsm.domain.parser.values.ParsingTable + +/** + * ParsingTable ํด๋ž˜์Šค๋ฅผ ์œ„ํ•œ ๋ถ„์„, ํ†ต๊ณ„, ์ถœ๋ ฅ ํ™•์žฅ ํ•จ์ˆ˜๋“ค์ž…๋‹ˆ๋‹ค. + * + * ์ด ํ™•์žฅ ํ•จ์ˆ˜๋“ค์€ ๊ฐœ๋ฐœ ๋„๊ตฌ, ๋””๋ฒ„๊น…, ๋ถ„์„ ๋ชฉ์ ์œผ๋กœ ์‚ฌ์šฉ๋˜๋ฉฐ, + * ํ•ต์‹ฌ ํŒŒ์‹ฑ ๋กœ์ง๊ณผ ๋ถ„๋ฆฌํ•˜์—ฌ ๊ด€์‹ฌ์‚ฌ๋ฅผ ๋ช…ํ™•ํžˆ ํ•ฉ๋‹ˆ๋‹ค. + * + * @author kangeunchan + * @since 2025.08.13 + */ + +/** + * ํ…Œ์ด๋ธ”์˜ ํฌ๊ธฐ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํฌ๊ธฐ ์ •๋ณด ๋งต + */ +fun ParsingTable.getSizeInfo(): Map = mapOf( + "stateCount" to states.size, + "actionCount" to actionTable.size, + "gotoCount" to gotoTable.size, + "terminalCount" to terminals.size, + "nonTerminalCount" to nonTerminals.size, + "acceptStateCount" to acceptStates.size +) + +/** + * ํ…Œ์ด๋ธ”์˜ ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰์„ ์ถ”์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”๋ชจ๋ฆฌ ์‚ฌ์šฉ๋Ÿ‰ ์ •๋ณด ๋งต (๋ฐ”์ดํŠธ) + */ +fun ParsingTable.getMemoryUsage(): Map { + val stateMemory = states.size * 1000L // ์ƒํƒœ๋‹น ๋Œ€๋žต 1KB + val actionMemory = actionTable.size * 100L // ์•ก์…˜๋‹น ๋Œ€๋žต 100B + val gotoMemory = gotoTable.size * 100L // goto๋‹น ๋Œ€๋žต 100B + + return mapOf( + "states" to stateMemory, + "actions" to actionMemory, + "gotos" to gotoMemory, + "total" to (stateMemory + actionMemory + gotoMemory) + ) +} + +/** + * ํ…Œ์ด๋ธ”์˜ ์••์ถ•๋ฅ ์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์••์ถ•๋ฅ  (0.0 ~ 1.0) + */ +fun ParsingTable.getCompressionRatio(): Double { + val totalCells = states.size * (terminals.size + nonTerminals.size) + val usedCells = actionTable.size + gotoTable.size + return if (totalCells > 0) 1.0 - (usedCells.toDouble() / totalCells) else 0.0 +} + +/** + * ํ…Œ์ด๋ธ”์„ ์••์ถ•ํ•ฉ๋‹ˆ๋‹ค (๋นˆ ์—”ํŠธ๋ฆฌ ์ œ๊ฑฐ). + * + * @return ์••์ถ•๋œ ํŒŒ์‹ฑ ํ…Œ์ด๋ธ” + */ +fun ParsingTable.compress(): ParsingTable { + // ์‹ค์ œ๋กœ๋Š” ์ด๋ฏธ ์••์ถ•๋œ ํ˜•ํƒœ์ด๋ฏ€๋กœ ์ž๊ธฐ ์ž์‹ ์„ ๋ฐ˜ํ™˜ + return this +} + +/** + * ํ…Œ์ด๋ธ”์„ ํ…์ŠคํŠธ ํ˜•ํƒœ๋กœ ์ถœ๋ ฅํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ…Œ์ด๋ธ” ๋ฌธ์ž์—ด + */ +fun ParsingTable.toTableString(): String = buildString { + appendLine("LR Parsing Table:") + appendLine("States: ${states.size}") + appendLine("Terminals: ${terminals.joinToString(", ")}") + appendLine("Non-terminals: ${nonTerminals.joinToString(", ")}") + appendLine() + + appendLine("Action Table:") + terminals.forEach { terminal -> + appendLine(" $terminal:") + states.keys.sorted().forEach { stateId -> + val action = getAction(stateId, terminal) + if (action != null) { + appendLine(" $stateId: $action") + } + } + } + + appendLine() + appendLine("Goto Table:") + nonTerminals.forEach { nonTerminal -> + appendLine(" $nonTerminal:") + states.keys.sorted().forEach { stateId -> + val goto = getGoto(stateId, nonTerminal) + if (goto != null) { + appendLine(" $stateId: $goto") + } + } + } + + val conflicts = getConflicts() + if (conflicts.isNotEmpty()) { + appendLine() + appendLine("Conflicts:") + conflicts.forEach { (type, details) -> + appendLine(" $type:") + details.forEach { detail -> + appendLine(" $detail") + } + } + } +} + +/** + * ํ…Œ์ด๋ธ”์˜ ํ†ต๊ณ„ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ†ต๊ณ„ ์ •๋ณด ๋งต + */ +fun ParsingTable.getStatistics(): Map = mapOf( + "stateCount" to states.size, + "actionEntries" to actionTable.size, + "gotoEntries" to gotoTable.size, + "terminalCount" to terminals.size, + "nonTerminalCount" to nonTerminals.size, + "acceptStateCount" to acceptStates.size, + "conflictCount" to getConflicts().values.sumOf { it.size }, + "isLR1Valid" to isLR1Valid(), + "compressionRatio" to getCompressionRatio(), + "memoryUsage" to (getMemoryUsage()[MemoryUsage.TOTAL] ?: 0L), + "averageActionsPerState" to if (states.isNotEmpty()) actionTable.size.toDouble() / states.size else 0.0, + "averageGotosPerState" to if (states.isNotEmpty()) gotoTable.size.toDouble() / states.size else 0.0 +) + +/** + * ํ…Œ์ด๋ธ”์˜ ์ƒ์„ธ ์š”์•ฝ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ธ ์š”์•ฝ ๋ฌธ์ž์—ด + */ +fun ParsingTable.toDetailedString(): String = buildString { + append("ParsingTable(") + append("states=${states.size}, ") + append("actions=${actionTable.size}, ") + append("gotos=${gotoTable.size}") + val conflictCount = getConflicts().values.sumOf { it.size } + if (conflictCount > 0) { + append(", conflicts=$conflictCount") + } + if (!isLR1Valid()) { + append(", INVALID") + } + append(")") +} + +object MemoryUsage { + const val TOTAL = "total" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/AggregateRoot.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/AggregateRoot.kt new file mode 100644 index 00000000..8ebbc549 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/AggregateRoot.kt @@ -0,0 +1,47 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ์ง‘ํ•ฉ ๋ฃจํŠธ(Aggregate Root)๋ฅผ ์œ„ํ•œ ์ถ”์ƒ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœํ–‰๊ณผ ์ผ๊ด€์„ฑ ๊ฒฝ๊ณ„ ๊ด€๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ์ง‘ํ•ฉ ๋ฃจํŠธ์˜ ์‹๋ณ„์ž ํƒ€์ž… + */ +abstract class AggregateRoot : EntityBase() { + + private val domainEvents = mutableListOf() + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param event ์ถ”๊ฐ€ํ•  ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ + */ + protected fun addDomainEvent(event: Any) { + domainEvents.add(event) + } + + /** + * ๋ฐœํ–‰๋˜์ง€ ์•Š์€ ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ชฉ๋ก + */ + fun getUncommittedEvents(): List = domainEvents.toList() + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋“ค์„ ๋ฐœํ–‰ ์™„๋ฃŒ๋กœ ํ‘œ์‹œํ•ฉ๋‹ˆ๋‹ค. + */ + fun markEventsAsCommitted() { + domainEvents.clear() + } + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ์˜ ๋ถˆ๋ณ€์‹์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ถˆ๋ณ€์‹์ด ๋งŒ์กฑ๋˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + abstract fun checkInvariants(): Boolean + + override fun isValid(): Boolean { + return checkInvariants() + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/AntiCorruptionLayer.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/AntiCorruptionLayer.kt new file mode 100644 index 00000000..f2228d7f --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/AntiCorruptionLayer.kt @@ -0,0 +1,450 @@ +package hs.kr.entrydsm.global.interfaces + +import hs.kr.entrydsm.global.values.Result + +/** + * Anti-Corruption Layer์˜ ๊ธฐ๋ณธ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Anti-Corruption Layer ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ์˜ ํ†ตํ•ฉ์—์„œ + * ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ๋ณดํ˜ธํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ์™ธ๋ถ€ ์‹œ์Šคํ…œ์˜ ๋ณ€๊ฒฝ์ด + * ๋‚ด๋ถ€ ๋„๋ฉ”์ธ์— ์˜ํ–ฅ์„ ์ฃผ์ง€ ์•Š๋„๋ก ๊ฒฉ๋ฆฌํ•˜๋ฉฐ, ๋„๋ฉ”์ธ ๋ชจ๋ธ์˜ ์ˆœ์ˆ˜์„ฑ์„ ์œ ์ง€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param Internal ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ํƒ€์ž… + * @param External ์™ธ๋ถ€ ์‹œ์Šคํ…œ ํƒ€์ž… + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface AntiCorruptionLayer : AntiCorruptionLayerMarker { + + /** + * ์™ธ๋ถ€ ๋ชจ๋ธ์„ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param external ์™ธ๋ถ€ ๋ชจ๋ธ + * @return ๋ณ€ํ™˜๋œ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ + */ + fun translateToInternal(external: External): Result + + /** + * ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ์™ธ๋ถ€ ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param internal ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ + * @return ๋ณ€ํ™˜๋œ ์™ธ๋ถ€ ๋ชจ๋ธ + */ + fun translateToExternal(internal: Internal): Result + + /** + * ๋ณ€ํ™˜ ๊ทœ์น™์ด ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun validateTranslationRules(): Boolean + + /** + * ์™ธ๋ถ€ ๋ชจ๋ธ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param external ๊ฒ€์ฆํ•  ์™ธ๋ถ€ ๋ชจ๋ธ + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun validateExternal(external: External): Boolean + + /** + * ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ์˜ ์œ ํšจ์„ฑ์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param internal ๊ฒ€์ฆํ•  ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun validateInternal(internal: Internal): Boolean + + /** + * ๋ณ€ํ™˜ ๋งคํ•‘ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋งคํ•‘ ์ •๋ณด + */ + fun getMappingInfo(): Map + + /** + * ์ง€์›๋˜๋Š” ์™ธ๋ถ€ ์‹œ์Šคํ…œ ๋ฒ„์ „์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์› ๋ฒ„์ „ ๋ชฉ๋ก + */ + fun getSupportedExternalVersions(): Set +} + +/** + * ๋ฐฐ์น˜ ๋ณ€ํ™˜์„ ์ง€์›ํ•˜๋Š” Anti-Corruption Layer ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface BatchAntiCorruptionLayer : AntiCorruptionLayer { + + /** + * ์™ธ๋ถ€ ๋ชจ๋ธ๋“ค์„ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋“ค๋กœ ์ผ๊ด„ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param externals ์™ธ๋ถ€ ๋ชจ๋ธ๋“ค + * @return ๋ณ€ํ™˜๋œ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋“ค + */ + fun translateToInternalBatch(externals: List): Result, TranslationError> + + /** + * ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋“ค์„ ์™ธ๋ถ€ ๋ชจ๋ธ๋“ค๋กœ ์ผ๊ด„ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param internals ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋“ค + * @return ๋ณ€ํ™˜๋œ ์™ธ๋ถ€ ๋ชจ๋ธ๋“ค + */ + fun translateToExternalBatch(internals: List): Result, TranslationError> + + /** + * ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ ์‹œ ์‚ฌ์šฉํ•  ์ฒญํฌ ํฌ๊ธฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฒญํฌ ํฌ๊ธฐ + */ + fun getChunkSize(): Int = 100 +} + +/** + * ์บ์‹œ๋ฅผ ์ง€์›ํ•˜๋Š” Anti-Corruption Layer ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface CacheableAntiCorruptionLayer : AntiCorruptionLayer { + + /** + * ๋ณ€ํ™˜ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œ์—์„œ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ์บ์‹œ ํ‚ค + * @return ์บ์‹œ๋œ ๋ณ€ํ™˜ ๊ฒฐ๊ณผ ๋˜๋Š” null + */ + fun getCachedTranslation(key: String): Result + + /** + * ๋ณ€ํ™˜ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ์บ์‹œ ํ‚ค + * @param value ์ €์žฅํ•  ๊ฐ’ + */ + fun cacheTranslation(key: String, value: Internal): Result + + /** + * ์บ์‹œ๋ฅผ ๋ฌดํšจํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ๋ฌดํšจํ™”ํ•  ์บ์‹œ ํ‚ค + */ + fun invalidateCache(key: String): Result + + /** + * ๋ชจ๋“  ์บ์‹œ๋ฅผ ์ง€์›๋‹ˆ๋‹ค. + */ + fun clearCache(): Result +} + +/** + * ๋น„๋™๊ธฐ ๋ณ€ํ™˜์„ ์ง€์›ํ•˜๋Š” Anti-Corruption Layer ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface AsyncAntiCorruptionLayer : AntiCorruptionLayer { + + /** + * ์™ธ๋ถ€ ๋ชจ๋ธ์„ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ ๋น„๋™๊ธฐ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param external ์™ธ๋ถ€ ๋ชจ๋ธ + * @return ๋ณ€ํ™˜๋œ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ + */ + suspend fun translateToInternalAsync(external: External): Result + + /** + * ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ์™ธ๋ถ€ ๋ชจ๋ธ๋กœ ๋น„๋™๊ธฐ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param internal ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ + * @return ๋ณ€ํ™˜๋œ ์™ธ๋ถ€ ๋ชจ๋ธ + */ + suspend fun translateToExternalAsync(internal: Internal): Result + + /** + * ์™ธ๋ถ€ ๋ชจ๋ธ๋“ค์„ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋“ค๋กœ ๋น„๋™๊ธฐ ์ผ๊ด„ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param externals ์™ธ๋ถ€ ๋ชจ๋ธ๋“ค + * @return ๋ณ€ํ™˜๋œ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋“ค + */ + suspend fun translateToInternalBatchAsync(externals: List): Result, TranslationError> + + /** + * ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋“ค์„ ์™ธ๋ถ€ ๋ชจ๋ธ๋“ค๋กœ ๋น„๋™๊ธฐ ์ผ๊ด„ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param internals ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋“ค + * @return ๋ณ€ํ™˜๋œ ์™ธ๋ถ€ ๋ชจ๋ธ๋“ค + */ + suspend fun translateToExternalBatchAsync(internals: List): Result, TranslationError> +} + +/** + * ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ์„ ์ง€์›ํ•˜๋Š” Anti-Corruption Layer ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface VersionedAntiCorruptionLayer : AntiCorruptionLayer { + + /** + * ํŠน์ • ๋ฒ„์ „์˜ ์™ธ๋ถ€ ๋ชจ๋ธ์„ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param external ์™ธ๋ถ€ ๋ชจ๋ธ + * @param version ์™ธ๋ถ€ ์‹œ์Šคํ…œ ๋ฒ„์ „ + * @return ๋ณ€ํ™˜๋œ ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ + */ + fun translateToInternal(external: External, version: String): Result + + /** + * ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ์„ ํŠน์ • ๋ฒ„์ „์˜ ์™ธ๋ถ€ ๋ชจ๋ธ๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param internal ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ๋ชจ๋ธ + * @param version ๋ชฉํ‘œ ์™ธ๋ถ€ ์‹œ์Šคํ…œ ๋ฒ„์ „ + * @return ๋ณ€ํ™˜๋œ ์™ธ๋ถ€ ๋ชจ๋ธ + */ + fun translateToExternal(internal: Internal, version: String): Result + + /** + * ํ˜„์žฌ ์ง€์›ํ•˜๋Š” ๊ธฐ๋ณธ ๋ฒ„์ „์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ธฐ๋ณธ ๋ฒ„์ „ + */ + fun getDefaultVersion(): String + + /** + * ๋ฒ„์ „ ๊ฐ„ ํ˜ธํ™˜์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param sourceVersion ์†Œ์Šค ๋ฒ„์ „ + * @param targetVersion ๋Œ€์ƒ ๋ฒ„์ „ + * @return ํ˜ธํ™˜๋˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isVersionCompatible(sourceVersion: String, targetVersion: String): Boolean +} + +/** + * ๋ณ€ํ™˜ ์˜ค๋ฅ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” sealed class์ž…๋‹ˆ๋‹ค. + */ +sealed class TranslationError( + val message: String, + val cause: Throwable? = null +) { + companion object { + private const val MAPPING_ERROR = "๋งคํ•‘ ๊ทœ์น™ ์˜ค๋ฅ˜" + private const val DATA_TYPE_ERROR = "๋ฐ์ดํ„ฐ ํƒ€์ž… ์˜ค๋ฅ˜" + private const val OMISSION_REQUIRE_FIELD = "ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ" + private const val VALID_ERROR = "๊ฒ€์ฆ ์˜ค๋ฅ˜" + private const val VERSION_ERROR = "๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ์˜ค๋ฅ˜" + private const val EXTERNAL_SYSTEM_ERROR = "์™ธ๋ถ€ ์‹œ์Šคํ…œ ์˜ค๋ฅ˜" + private const val CONFIGURATION_ERROR = "์„ค์ • ์˜ค๋ฅ˜" + private const val UNKNOWN_ERROR = "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜" + } + + /** + * ๋งคํ•‘ ๊ทœ์น™ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class MappingRuleError(val field: String, val reason: String) : + TranslationError("$MAPPING_ERROR [$field]: $reason") + + /** + * ๋ฐ์ดํ„ฐ ํƒ€์ž… ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class DataTypeError(val expectedType: String, val actualType: String) : + TranslationError("$DATA_TYPE_ERROR: ์˜ˆ์ƒ $expectedType, ์‹ค์ œ $actualType") + + /** + * ํ•„์ˆ˜ ํ•„๋“œ ๋ˆ„๋ฝ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class MissingFieldError(val fieldName: String) : + TranslationError("$OMISSION_REQUIRE_FIELD: $fieldName") + + /** + * ๊ฒ€์ฆ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class ValidationError(val violations: List) : + TranslationError("$VALID_ERROR: ${violations.joinToString(", ")}") + + /** + * ๋ฒ„์ „ ํ˜ธํ™˜์„ฑ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class VersionCompatibilityError(val sourceVersion: String, val targetVersion: String) : + TranslationError("$VERSION_ERROR: $sourceVersion -> $targetVersion") + + /** + * ์™ธ๋ถ€ ์‹œ์Šคํ…œ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class ExternalSystemError(val systemName: String, val reason: String, val throwable: Throwable? = null) : + TranslationError("$EXTERNAL_SYSTEM_ERROR [$systemName]: $reason", throwable) + + /** + * ์„ค์ • ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class ConfigurationError(val reason: String) : + TranslationError("$CONFIGURATION_ERROR: $reason") + + /** + * ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class UnknownError(val reason: String, val throwable: Throwable? = null) : + TranslationError("$UNKNOWN_ERROR: $reason", throwable) +} + +/** + * ๋ณ€ํ™˜ ๊ทœ์น™์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface TranslationRule { + + /** + * ๋ณ€ํ™˜ ๊ทœ์น™์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param source ์†Œ์Šค ๊ฐ์ฒด + * @return ๋ณ€ํ™˜๋œ ๊ฐ์ฒด + */ + fun apply(source: External): Result + + /** + * ์—ญ๋ฐฉํ–ฅ ๋ณ€ํ™˜ ๊ทœ์น™์„ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param source ์†Œ์Šค ๊ฐ์ฒด + * @return ๋ณ€ํ™˜๋œ ๊ฐ์ฒด + */ + fun applyReverse(source: Internal): Result + + /** + * ๋ณ€ํ™˜ ๊ทœ์น™์ด ์ ์šฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param source ์†Œ์Šค ๊ฐ์ฒด + * @return ์ ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isApplicable(source: External): Boolean + + /** + * ์—ญ๋ฐฉํ–ฅ ๋ณ€ํ™˜ ๊ทœ์น™์ด ์ ์šฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param source ์†Œ์Šค ๊ฐ์ฒด + * @return ์ ์šฉ ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isReverseApplicable(source: Internal): Boolean + + /** + * ๋ณ€ํ™˜ ๊ทœ์น™์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ์šฐ์„ ) + */ + fun getPriority(): Int = 0 +} + +/** + * ๋ณ€ํ™˜ ์ปจํ…์ŠคํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class TranslationContext( + val sourceSystem: String, + val targetSystem: String, + val version: String, + val metadata: Map = emptyMap(), + val options: TranslationOptions = TranslationOptions() +) { + + /** + * ์ปจํ…์ŠคํŠธ์— ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. + * + * @param key ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ํ‚ค + * @param value ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๊ฐ’ + * @return ์ƒˆ๋กœ์šด TranslationContext + */ + fun withMetadata(key: String, value: Any): TranslationContext = + copy(metadata = metadata + (key to value)) + + /** + * ์ปจํ…์ŠคํŠธ์˜ ์˜ต์…˜์„ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค. + * + * @param newOptions ์ƒˆ๋กœ์šด ์˜ต์…˜ + * @return ์ƒˆ๋กœ์šด TranslationContext + */ + fun withOptions(newOptions: TranslationOptions): TranslationContext = + copy(options = newOptions) +} + +/** + * ๋ณ€ํ™˜ ์˜ต์…˜์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class TranslationOptions( + val strictMode: Boolean = false, + val ignoreUnknownFields: Boolean = true, + val useCache: Boolean = true, + val timeout: Long = 30000, // 30์ดˆ + val retryCount: Int = 3, + val validateResult: Boolean = true +) { + + companion object { + /** + * ๊ธฐ๋ณธ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun default(): TranslationOptions = TranslationOptions() + + /** + * ์—„๊ฒฉ ๋ชจ๋“œ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun strict(): TranslationOptions = TranslationOptions( + strictMode = true, + ignoreUnknownFields = false, + validateResult = true + ) + + /** + * ๊ด€๋Œ€ํ•œ ๋ชจ๋“œ ์˜ต์…˜์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + fun lenient(): TranslationOptions = TranslationOptions( + strictMode = false, + ignoreUnknownFields = true, + validateResult = false + ) + } +} + +/** + * Anti-Corruption Layer ํŒฉํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface AntiCorruptionLayerFactory { + + /** + * ์ง€์ •๋œ ํƒ€์ž…๋“ค์— ๋Œ€ํ•œ Anti-Corruption Layer๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param internalType ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ํƒ€์ž… + * @param externalType ์™ธ๋ถ€ ์‹œ์Šคํ…œ ํƒ€์ž… + * @return Anti-Corruption Layer ์ธ์Šคํ„ด์Šค + */ + fun create( + internalType: Class, + externalType: Class + ): AntiCorruptionLayer + + /** + * ๋ฐฐ์น˜ ์ฒ˜๋ฆฌ๋ฅผ ์ง€์›ํ•˜๋Š” Anti-Corruption Layer๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param internalType ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ํƒ€์ž… + * @param externalType ์™ธ๋ถ€ ์‹œ์Šคํ…œ ํƒ€์ž… + * @return ๋ฐฐ์น˜ Anti-Corruption Layer ์ธ์Šคํ„ด์Šค + */ + fun createBatch( + internalType: Class, + externalType: Class + ): BatchAntiCorruptionLayer + + /** + * ์บ์‹œ๋ฅผ ์ง€์›ํ•˜๋Š” Anti-Corruption Layer๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param internalType ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ํƒ€์ž… + * @param externalType ์™ธ๋ถ€ ์‹œ์Šคํ…œ ํƒ€์ž… + * @return ์บ์‹œ ์ง€์› Anti-Corruption Layer ์ธ์Šคํ„ด์Šค + */ + fun createCacheable( + internalType: Class, + externalType: Class + ): CacheableAntiCorruptionLayer + + /** + * ๋น„๋™๊ธฐ๋ฅผ ์ง€์›ํ•˜๋Š” Anti-Corruption Layer๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param internalType ๋‚ด๋ถ€ ๋„๋ฉ”์ธ ํƒ€์ž… + * @param externalType ์™ธ๋ถ€ ์‹œ์Šคํ…œ ํƒ€์ž… + * @return ๋น„๋™๊ธฐ Anti-Corruption Layer ์ธ์Šคํ„ด์Šค + */ + fun createAsync( + internalType: Class, + externalType: Class + ): AsyncAntiCorruptionLayer +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/CreationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/CreationStrategy.kt new file mode 100644 index 00000000..61e9433b --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/CreationStrategy.kt @@ -0,0 +1,27 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ์ƒ์„ฑ ์ „๋žต์„ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ๊ฐ์ฒด ์ƒ์„ฑ์˜ ๋ณต์žกํ•œ ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ์ƒ์„ฑํ•  ๊ฐ์ฒด์˜ ํƒ€์ž… + */ +interface CreationStrategy { + + /** + * ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ๋ฐ›์•„ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param params ์ƒ์„ฑ ๋งค๊ฐœ๋ณ€์ˆ˜๋“ค + * @return ์ƒ์„ฑ๋œ ๊ฐ์ฒด + */ + fun create(vararg params: Any?): T + + /** + * ์ƒ์„ฑ ์ „๋žต์ด ์ฃผ์–ด์ง„ ๋งค๊ฐœ๋ณ€์ˆ˜๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param params ํ™•์ธํ•  ๋งค๊ฐœ๋ณ€์ˆ˜๋“ค + * @return ์ง€์›ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun supports(vararg params: Any?): Boolean +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/DomainMarker.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/DomainMarker.kt new file mode 100644 index 00000000..359ff6a3 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/DomainMarker.kt @@ -0,0 +1,324 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ์‹๋ณ„ํ•˜๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD ์•„ํ‚คํ…์ฒ˜์—์„œ ๋„๋ฉ”์ธ ๊ณ„์ธต์˜ ๊ฐ์ฒด๋“ค์„ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œ์‹œํ•˜๊ธฐ ์œ„ํ•œ + * ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. ์ปดํŒŒ์ผ ํƒ€์ž„๊ณผ ๋Ÿฐํƒ€์ž„์— ๋„๋ฉ”์ธ ๊ฐ์ฒด๋ฅผ ๊ตฌ๋ถ„ํ•˜๊ณ  + * ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๊ฒŒ ํ•ด์ฃผ๋ฉฐ, ๋„๋ฉ”์ธ ๊ทœ์น™์˜ ์ ์šฉ ๋ฒ”์œ„๋ฅผ ๋ช…ํ™•ํžˆ ํ•ฉ๋‹ˆ๋‹ค. + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface DomainMarker { + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ์ปจํ…์ŠคํŠธ (์˜ˆ: "lexer", "parser", "evaluator" ๋“ฑ) + */ + fun getDomainContext(): String + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ๊ฐ์ฒด ํƒ€์ž… (์˜ˆ: "aggregate", "entity", "value", "service" ๋“ฑ) + */ + fun getDomainType(): String + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValid(): Boolean = true + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์‹๋ณ„์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ์ฒด ์‹๋ณ„์ž ๋˜๋Š” null + */ + fun getIdentifier(): String? = null + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ๋ฒ„์ „์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ์ฒด ๋ฒ„์ „ + */ + fun getVersion(): Long = 1L + + /** + * ๋„๋ฉ”์ธ ๊ทœ์น™์ด ์ ์šฉ๋˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ๊ทœ์น™์ด ์ ์šฉ๋˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isDomainRuleApplicable(): Boolean = true + + /** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐœ์ƒ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ ๋ฐœ์ƒ ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun canRaiseDomainEvents(): Boolean = false + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์ƒํƒœ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒํƒœ ์ •๋ณด ๋งต + */ + fun getDomainState(): Map = emptyMap() +} + +/** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface AggregateRootMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.AGGREGATE + + override fun canRaiseDomainEvents(): Boolean = true + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ์˜ ๊ฒฝ๊ณ„๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง‘ํ•ฉ ๊ฒฝ๊ณ„ ๋‚ด์˜ ์—”ํ‹ฐํ‹ฐ ๋ฐ ๊ฐ’ ๊ฐ์ฒด ํƒ€์ž…๋“ค + */ + fun getAggregateBoundary(): Set = emptySet() + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ์˜ ๋ถˆ๋ณ€ ์กฐ๊ฑด์„ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ถˆ๋ณ€ ์กฐ๊ฑด์ด ๋งŒ์กฑ๋˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun checkInvariants(): Boolean = true +} + +/** + * ์—”ํ‹ฐํ‹ฐ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface EntityMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.ENTITY + + /** + * ์—”ํ‹ฐํ‹ฐ์˜ ๊ณ ์œ  ์‹๋ณ„์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—”ํ‹ฐํ‹ฐ ์‹๋ณ„์ž + */ + override fun getIdentifier(): String + + /** + * ์—”ํ‹ฐํ‹ฐ์˜ ๋™๋“ฑ์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * ์—”ํ‹ฐํ‹ฐ๋Š” ์‹๋ณ„์ž๋กœ๋งŒ ๋น„๊ต๋ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๊ฐ์ฒด + * @return ๊ฐ™์€ ์—”ํ‹ฐํ‹ฐ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSameEntity(other: EntityMarker): Boolean = + this.getIdentifier() == other.getIdentifier() +} + +/** + * ๊ฐ’ ๊ฐ์ฒด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface ValueObjectMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.VALUE + + override fun getIdentifier(): String? = null + + /** + * ๊ฐ’ ๊ฐ์ฒด์˜ ๋™๋“ฑ์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * ๊ฐ’ ๊ฐ์ฒด๋Š” ๋ชจ๋“  ์†์„ฑ ๊ฐ’์œผ๋กœ ๋น„๊ต๋ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๊ฐ์ฒด + * @return ๊ฐ™์€ ๊ฐ’์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isSameValue(other: ValueObjectMarker): Boolean = this == other + + /** + * ๊ฐ’ ๊ฐ์ฒด๊ฐ€ ๋ถˆ๋ณ€์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ถˆ๋ณ€์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isImmutable(): Boolean = true +} + +/** + * ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface DomainServiceMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.SERVICE + + /** + * ๋„๋ฉ”์ธ ์„œ๋น„์Šค๊ฐ€ ์ƒํƒœ๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒํƒœ๋ฅผ ๊ฐ€์ง€๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isStateful(): Boolean = false + + /** + * ๋„๋ฉ”์ธ ์„œ๋น„์Šค์˜ ์ˆ˜ํ–‰ ๊ฐ€๋Šฅํ•œ ์—ฐ์‚ฐ๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์—ฐ์‚ฐ ์ด๋ฆ„๋“ค + */ + fun getAvailableOperations(): Set = emptySet() +} + +/** + * ํŒฉํ† ๋ฆฌ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface FactoryMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.FACTORY + + /** + * ํŒฉํ† ๋ฆฌ๊ฐ€ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š” ๊ฐ์ฒด ํƒ€์ž…๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ฑ ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด ํƒ€์ž…๋“ค + */ + fun getCreatableTypes(): Set = emptySet() + + /** + * ํŒฉํ† ๋ฆฌ์˜ ๋ณต์žก๋„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณต์žก๋„ ์ˆ˜์ค€ + */ + fun getComplexity(): String = DomainMarkerObject.SIMPLE +} + +/** + * ์ •์ฑ…์„ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface PolicyMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.POLICY + + /** + * ์ •์ฑ…์˜ ์ ์šฉ ๋ฒ”์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ •์ฑ… ์ ์šฉ ๋ฒ”์œ„ + */ + fun getPolicyScope(): String = DomainMarkerObject.DOMAIN + + /** + * ์ •์ฑ…์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์„ ์ˆœ์œ„ (๋†’์„์ˆ˜๋ก ์šฐ์„ ) + */ + fun getPriority(): Int = 0 +} + +/** + * ๋ช…์„ธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface SpecificationMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.SPECIFICATION + + /** + * ๋ช…์„ธ์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์„ ์ˆœ์œ„ + */ + fun getSpecificationPriority(): String = DomainMarkerObject.NORMAL + + /** + * ๋ช…์„ธ๊ฐ€ ์กฐํ•ฉ ๊ฐ€๋Šฅํ•œ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์กฐํ•ฉ ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isCombinable(): Boolean = true +} + +/** + * ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface RepositoryMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.REPOSITORY + + /** + * ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๊ฐ€ ๊ด€๋ฆฌํ•˜๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง‘ํ•ฉ ๋ฃจํŠธ ํƒ€์ž… + */ + fun getAggregateType(): String + + /** + * ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๊ฐ€ ์บ์‹œ๋ฅผ ์ง€์›ํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์บ์‹œ ์ง€์›ํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun supportsCaching(): Boolean = false +} + +/** + * ๋„๋ฉ”์ธ ์ด๋ฒคํŠธ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface DomainEventMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.EVENT + + /** + * ์ด๋ฒคํŠธ์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ด๋ฒคํŠธ ํƒ€์ž… + */ + fun getEventType(): String + + /** + * ์ด๋ฒคํŠธ์˜ ๋ฐœ์ƒ ์‹œ๊ฐ์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฐœ์ƒ ์‹œ๊ฐ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun getOccurredAt(): Long + + /** + * ์ด๋ฒคํŠธ๊ฐ€ ์ฒ˜๋ฆฌ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ฒ˜๋ฆฌ๋˜์—ˆ์œผ๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isProcessed(): Boolean = false +} + +/** + * Anti-Corruption Layer๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface AntiCorruptionLayerMarker : DomainMarker { + + override fun getDomainType(): String = DomainMarkerObject.ANTI_CORRUPTION_LAYER + + /** + * ๋ณดํ˜ธํ•˜๋Š” ๋„๋ฉ”์ธ ์ปจํ…์ŠคํŠธ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ณดํ˜ธ ๋Œ€์ƒ ๋„๋ฉ”์ธ ์ปจํ…์ŠคํŠธ + */ + fun getProtectedDomain(): String + + /** + * ์™ธ๋ถ€ ์‹œ์Šคํ…œ๊ณผ์˜ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์™ธ๋ถ€ ์ธํ„ฐํŽ˜์ด์Šค ์ •๋ณด + */ + fun getExternalInterface(): Map = emptyMap() +} + +object DomainMarkerObject{ + const val AGGREGATE = "aggregate" + const val SERVICE = "service" + const val ENTITY = "entity" + const val VALUE = "value" + const val FACTORY = "factory" + const val SIMPLE = "simple" + const val POLICY = "policy" + const val DOMAIN = "DOMAIN" + const val SPECIFICATION = "specification" + const val NORMAL = "NORMAL" + const val REPOSITORY = "repository" + const val EVENT = "event" + const val ANTI_CORRUPTION_LAYER = "anti-corruption-layer" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/DomainObject.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/DomainObject.kt new file mode 100644 index 00000000..4fee1902 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/DomainObject.kt @@ -0,0 +1,44 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ๋ชจ๋“  ๋„๋ฉ”์ธ ๊ฐ์ฒด๊ฐ€ ๊ตฌํ˜„ํ•ด์•ผ ํ•˜๋Š” ์ตœ์ƒ์œ„ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD์˜ ๋ชจ๋“  ๋„๋ฉ”์ธ ๊ฐ์ฒด(Aggregate, Entity, Value Object)๊ฐ€ + * ๊ณตํ†ต์œผ๋กœ ๊ฐ€์ ธ์•ผ ํ•˜๋Š” ๊ธฐ๋ณธ์ ์ธ ์‹๋ณ„ ๋ฐ ๋™๋“ฑ์„ฑ ๋น„๊ต ๊ธฐ๋Šฅ์„ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + * ๊ทนํ•œ ์ถ”์ƒํ™”๋ฅผ ํ†ตํ•ด ๊ฒฐํ•ฉ๋„๋ฅผ ์ตœ์†Œํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ์‹๋ณ„์ž ํƒ€์ž… + * + * @author kangeunchan + * @since 2025.07.28 + */ +interface DomainObject { + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ๊ณ ์œ  ์‹๋ณ„์ž๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ์ฒด์˜ ์‹๋ณ„์ž + */ + fun getId(): T + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ํƒ€์ž…์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๊ฐ์ฒด ํƒ€์ž… ๋ฌธ์ž์—ด + */ + fun getType(): String + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด๊ฐ€ ์œ ํšจํ•œ์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์œ ํšจํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isValid(): Boolean + + /** + * ๋„๋ฉ”์ธ ๊ฐ์ฒด์˜ ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋งต + */ + fun getMetadata(): Map = emptyMap() +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/DomainService.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/DomainService.kt new file mode 100644 index 00000000..a99edeab --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/DomainService.kt @@ -0,0 +1,23 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ๋„๋ฉ”์ธ ์„œ๋น„์Šค๋ฅผ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋„๋ฉ”์ธ ๋กœ์ง์„ ์บก์Аํ™”ํ•˜๋˜ ์ƒํƒœ๋Š” ๊ฐ€์ง€์ง€ ์•Š๋Š” ์„œ๋น„์Šค๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. + */ +interface DomainService { + + /** + * ์„œ๋น„์Šค ์ด๋ฆ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„œ๋น„์Šค ์ด๋ฆ„ + */ + fun getServiceName(): String + + /** + * ์„œ๋น„์Šค๊ฐ€ ์ง€์›ํ•˜๋Š” ์ž‘์—…๋“ค์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง€์›ํ•˜๋Š” ์ž‘์—… ๋ชฉ๋ก + */ + fun getSupportedOperations(): Set +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/EntityBase.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/EntityBase.kt new file mode 100644 index 00000000..5295bc5b --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/EntityBase.kt @@ -0,0 +1,57 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ์—”ํ‹ฐํ‹ฐ(Entity)๋ฅผ ์œ„ํ•œ ์ถ”์ƒ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * ์‹๋ณ„์ž ๊ธฐ๋ฐ˜ ๋™๋“ฑ์„ฑ๊ณผ ์ƒ๋ช…์ฃผ๊ธฐ ๊ด€๋ฆฌ๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ์—”ํ‹ฐํ‹ฐ์˜ ์‹๋ณ„์ž ํƒ€์ž… + */ +abstract class EntityBase : DomainObject { + + private var _createdAt: Long = System.currentTimeMillis() + private var _modifiedAt: Long = System.currentTimeMillis() + + /** + * ์—”ํ‹ฐํ‹ฐ์˜ ์ƒ์„ฑ ์‹œ๊ฐ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ƒ์„ฑ ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun getCreatedAt(): Long = _createdAt + + /** + * ์—”ํ‹ฐํ‹ฐ์˜ ๋งˆ์ง€๋ง‰ ์ˆ˜์ • ์‹œ๊ฐ„์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ˆ˜์ • ์‹œ๊ฐ„ (๋ฐ€๋ฆฌ์ดˆ) + */ + fun getModifiedAt(): Long = _modifiedAt + + /** + * ์—”ํ‹ฐํ‹ฐ์˜ ์ˆ˜์ • ์‹œ๊ฐ„์„ ์—…๋ฐ์ดํŠธํ•ฉ๋‹ˆ๋‹ค. + */ + protected fun updateModifiedTime() { + _modifiedAt = System.currentTimeMillis() + } + + /** + * ์—”ํ‹ฐํ‹ฐ์˜ ๋™๋“ฑ์„ฑ์€ ์‹๋ณ„์ž๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + */ + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is EntityBase<*>) return false + return getId() == other.getId() + } + + /** + * ์—”ํ‹ฐํ‹ฐ์˜ ํ•ด์‹œ ์ฝ”๋“œ๋Š” ์‹๋ณ„์ž๋กœ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + */ + override fun hashCode(): Int { + return getId()?.hashCode() ?: 0 + } + + override fun getMetadata(): Map = mapOf( + "createdAt" to _createdAt, + "modifiedAt" to _modifiedAt, + "type" to getType() + ) +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ProcessingResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ProcessingResult.kt new file mode 100644 index 00000000..95b2dd88 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ProcessingResult.kt @@ -0,0 +1,18 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class ProcessingResult( + val success: Boolean, + val result: T? = null, + val errors: List = emptyList(), + val metadata: Map = emptyMap() +) { + companion object { + fun success(result: T, metadata: Map = emptyMap()) = + ProcessingResult(true, result, emptyList(), metadata) + fun failure(errors: List) = + ProcessingResult(false, null, errors) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ProcessingStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ProcessingStrategy.kt new file mode 100644 index 00000000..40aaacd9 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ProcessingStrategy.kt @@ -0,0 +1,28 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ์ฒ˜๋ฆฌ ์ „๋žต์„ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋‹ค์–‘ํ•œ ์ฒ˜๋ฆฌ ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param Input ์ž…๋ ฅ ํƒ€์ž… + * @param Output ์ถœ๋ ฅ ํƒ€์ž… + */ +interface ProcessingStrategy { + + /** + * ์ž…๋ ฅ์„ ์ฒ˜๋ฆฌํ•˜์—ฌ ์ถœ๋ ฅ์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ์ฒ˜๋ฆฌํ•  ์ž…๋ ฅ + * @return ์ฒ˜๋ฆฌ ๊ฒฐ๊ณผ + */ + fun process(input: Input): ProcessingResult + + /** + * ์ฒ˜๋ฆฌ ์ „๋žต์ด ์ฃผ์–ด์ง„ ์ž…๋ ฅ์„ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param input ํ™•์ธํ•  ์ž…๋ ฅ + * @return ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun canProcess(input: Input): Boolean +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/Repository.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/Repository.kt new file mode 100644 index 00000000..225b6b49 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/Repository.kt @@ -0,0 +1,471 @@ +package hs.kr.entrydsm.global.interfaces + +import hs.kr.entrydsm.global.values.Result + +/** + * ๋„๋ฉ”์ธ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ์˜ ๊ธฐ๋ณธ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * DDD Repository ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์ง‘ํ•ฉ ๋ฃจํŠธ์˜ ์˜์†์„ฑ์„ ๋‹ด๋‹นํ•˜๋Š” + * ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ •์˜ํ•ฉ๋‹ˆ๋‹ค. ๋„๋ฉ”์ธ ๊ณ„์ธต์—์„œ ์ธํ”„๋ผ์ŠคํŠธ๋Ÿญ์ฒ˜ ๊ณ„์ธต์˜ + * ๊ตฌ์ฒด์ ์ธ ์ €์žฅ์†Œ ๊ตฌํ˜„๊ณผ ๋ถ„๋ฆฌํ•˜์—ฌ ์˜์กด์„ฑ ์—ญ์ „์„ ์‹คํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๊ด€๋ฆฌํ•˜๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ์˜ ํƒ€์ž… + * @param ID ์ง‘ํ•ฉ ๋ฃจํŠธ ์‹๋ณ„์ž์˜ ํƒ€์ž… + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +interface Repository : RepositoryMarker { + + /** + * ์‹๋ณ„์ž๋กœ ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ง‘ํ•ฉ ๋ฃจํŠธ ์‹๋ณ„์ž + * @return ์ง‘ํ•ฉ ๋ฃจํŠธ ๋˜๋Š” null + */ + suspend fun findById(id: ID): Result + + /** + * ์‹๋ณ„์ž๋กœ ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @param id ์ง‘ํ•ฉ ๋ฃจํŠธ ์‹๋ณ„์ž + * @return ์ง‘ํ•ฉ ๋ฃจํŠธ ๋˜๋Š” null + */ + fun findByIdSync(id: ID): Result + + /** + * ๋ชจ๋“  ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง‘ํ•ฉ ๋ฃจํŠธ ๋ชฉ๋ก + */ + suspend fun findAll(): Result, RepositoryError> + + /** + * ๋ชจ๋“  ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @return ์ง‘ํ•ฉ ๋ฃจํŠธ ๋ชฉ๋ก + */ + fun findAllSync(): Result, RepositoryError> + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregate ์ €์žฅํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ + * @return ์ €์žฅ๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ + */ + suspend fun save(aggregate: T): Result + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @param aggregate ์ €์žฅํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ + * @return ์ €์žฅ๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ + */ + fun saveSync(aggregate: T): Result + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค์„ ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregates ์ €์žฅํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค + * @return ์ €์žฅ๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค + */ + suspend fun saveAll(aggregates: List): Result, RepositoryError> + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค์„ ์ผ๊ด„ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @param aggregates ์ €์žฅํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค + * @return ์ €์žฅ๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค + */ + fun saveAllSync(aggregates: List): Result, RepositoryError> + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์‚ญ์ œํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ ์‹๋ณ„์ž + * @return ์‚ญ์ œ ์„ฑ๊ณต ์—ฌ๋ถ€ + */ + suspend fun deleteById(id: ID): Result + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @param id ์‚ญ์ œํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ ์‹๋ณ„์ž + * @return ์‚ญ์ œ ์„ฑ๊ณต ์—ฌ๋ถ€ + */ + fun deleteByIdSync(id: ID): Result + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregate ์‚ญ์ œํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ + * @return ์‚ญ์ œ ์„ฑ๊ณต ์—ฌ๋ถ€ + */ + suspend fun delete(aggregate: T): Result + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์‚ญ์ œํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @param aggregate ์‚ญ์ œํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ + * @return ์‚ญ์ œ ์„ฑ๊ณต ์—ฌ๋ถ€ + */ + fun deleteSync(aggregate: T): Result + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ํ™•์ธํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ ์‹๋ณ„์ž + * @return ์กด์žฌ ์—ฌ๋ถ€ + */ + suspend fun existsById(id: ID): Result + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @param id ํ™•์ธํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ ์‹๋ณ„์ž + * @return ์กด์žฌ ์—ฌ๋ถ€ + */ + fun existsByIdSync(id: ID): Result + + /** + * ์ €์žฅ๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ์˜ ์ด ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์ง‘ํ•ฉ ๋ฃจํŠธ ๊ฐœ์ˆ˜ + */ + suspend fun count(): Result + + /** + * ์ €์žฅ๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ์˜ ์ด ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @return ์ง‘ํ•ฉ ๋ฃจํŠธ ๊ฐœ์ˆ˜ + */ + fun countSync(): Result + + /** + * ์กฐ๊ฑด์— ๋งž๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param specification ์กฐํšŒ ์กฐ๊ฑด + * @return ์กฐ๊ฑด์— ๋งž๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ ๋ชฉ๋ก + */ + suspend fun findBySpecification(specification: RepositorySpecification): Result, RepositoryError> + + /** + * ์กฐ๊ฑด์— ๋งž๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @param specification ์กฐํšŒ ์กฐ๊ฑด + * @return ์กฐ๊ฑด์— ๋งž๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ ๋ชฉ๋ก + */ + fun findBySpecificationSync(specification: RepositorySpecification): Result, RepositoryError> + + /** + * ํŽ˜์ด์ง•๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param pageNumber ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + * @param pageSize ํŽ˜์ด์ง€ ํฌ๊ธฐ + * @return ํŽ˜์ด์ง•๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค + */ + suspend fun findPaged(pageNumber: Int, pageSize: Int): Result, RepositoryError> + + /** + * ํŽ˜์ด์ง•๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. (๋™๊ธฐ ๋ฒ„์ „) + * + * @param pageNumber ํŽ˜์ด์ง€ ๋ฒˆํ˜ธ (0๋ถ€ํ„ฐ ์‹œ์ž‘) + * @param pageSize ํŽ˜์ด์ง€ ํฌ๊ธฐ + * @return ํŽ˜์ด์ง•๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค + */ + fun findPagedSync(pageNumber: Int, pageSize: Int): Result, RepositoryError> +} + +/** + * ์ฝ๊ธฐ ์ „์šฉ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface ReadOnlyRepository : RepositoryMarker { + + /** + * ์‹๋ณ„์ž๋กœ ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + suspend fun findById(id: ID): Result + + /** + * ๋ชจ๋“  ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + suspend fun findAll(): Result, RepositoryError> + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + */ + suspend fun existsById(id: ID): Result + + /** + * ์ €์žฅ๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ์˜ ์ด ๊ฐœ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + */ + suspend fun count(): Result + + /** + * ์กฐ๊ฑด์— ๋งž๋Š” ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + suspend fun findBySpecification(specification: RepositorySpecification): Result, RepositoryError> + + /** + * ํŽ˜์ด์ง•๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ๋“ค์„ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + */ + suspend fun findPaged(pageNumber: Int, pageSize: Int): Result, RepositoryError> +} + +/** + * ์บ์‹œ๋ฅผ ์ง€์›ํ•˜๋Š” ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface CacheableRepository : Repository { + + override fun supportsCaching(): Boolean = true + + /** + * ์บ์‹œ์—์„œ ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์กฐํšŒํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ง‘ํ•ฉ ๋ฃจํŠธ ์‹๋ณ„์ž + * @return ์บ์‹œ๋œ ์ง‘ํ•ฉ ๋ฃจํŠธ ๋˜๋Š” null + */ + suspend fun findFromCache(id: ID): Result + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์บ์‹œ์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregate ์บ์‹œํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ + */ + suspend fun putToCache(aggregate: T): Result + + /** + * ์บ์‹œ์—์„œ ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์ œ๊ฑฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param id ์ œ๊ฑฐํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ ์‹๋ณ„์ž + */ + suspend fun evictFromCache(id: ID): Result + + /** + * ๋ชจ๋“  ์บ์‹œ๋ฅผ ์ง€์›๋‹ˆ๋‹ค. + */ + suspend fun clearCache(): Result + + /** + * ์บ์‹œ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์บ์‹œ ํ†ต๊ณ„ ์ •๋ณด + */ + suspend fun getCacheStatistics(): Result +} + +/** + * ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์กฐํšŒ ์กฐ๊ฑด์„ ์ •์˜ํ•˜๋Š” ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface RepositorySpecification { + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๊ฐ€ ์กฐ๊ฑด์„ ๋งŒ์กฑํ•˜๋Š”์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregate ํ™•์ธํ•  ์ง‘ํ•ฉ ๋ฃจํŠธ + * @return ์กฐ๊ฑด ๋งŒ์กฑ ์—ฌ๋ถ€ + */ + fun isSatisfiedBy(aggregate: T): Boolean + + /** + * ๋‹ค๋ฅธ ์กฐ๊ฑด๊ณผ AND ์—ฐ์‚ฐ์œผ๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๊ฒฐํ•ฉํ•  ์กฐ๊ฑด + * @return ๊ฒฐํ•ฉ๋œ ์กฐ๊ฑด + */ + fun and(other: RepositorySpecification): RepositorySpecification = + AndRepositorySpecification(this, other) + + /** + * ๋‹ค๋ฅธ ์กฐ๊ฑด๊ณผ OR ์—ฐ์‚ฐ์œผ๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๊ฒฐํ•ฉํ•  ์กฐ๊ฑด + * @return ๊ฒฐํ•ฉ๋œ ์กฐ๊ฑด + */ + fun or(other: RepositorySpecification): RepositorySpecification = + OrRepositorySpecification(this, other) + + /** + * ์กฐ๊ฑด์„ ๋ถ€์ •ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋ถ€์ •๋œ ์กฐ๊ฑด + */ + fun not(): RepositorySpecification = NotRepositorySpecification(this) +} + +/** + * AND ์กฐ๊ฑด์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +class AndRepositorySpecification( + private val left: RepositorySpecification, + private val right: RepositorySpecification +) : RepositorySpecification { + + override fun isSatisfiedBy(aggregate: T): Boolean = + left.isSatisfiedBy(aggregate) && right.isSatisfiedBy(aggregate) +} + +/** + * OR ์กฐ๊ฑด์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +class OrRepositorySpecification( + private val left: RepositorySpecification, + private val right: RepositorySpecification +) : RepositorySpecification { + + override fun isSatisfiedBy(aggregate: T): Boolean = + left.isSatisfiedBy(aggregate) || right.isSatisfiedBy(aggregate) +} + +/** + * NOT ์กฐ๊ฑด์„ ๊ตฌํ˜„ํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +class NotRepositorySpecification( + private val specification: RepositorySpecification +) : RepositorySpecification { + + override fun isSatisfiedBy(aggregate: T): Boolean = + !specification.isSatisfiedBy(aggregate) +} + +/** + * ํŽ˜์ด์ง• ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class PagedResult( + val content: List, + val pageNumber: Int, + val pageSize: Int, + val totalElements: Long, + val totalPages: Int, + val isFirst: Boolean, + val isLast: Boolean, + val hasNext: Boolean, + val hasPrevious: Boolean +) { + companion object { + fun of( + content: List, + pageNumber: Int, + pageSize: Int, + totalElements: Long + ): PagedResult { + val totalPages = if (totalElements == 0L) 0 else ((totalElements - 1) / pageSize + 1).toInt() + return PagedResult( + content = content, + pageNumber = pageNumber, + pageSize = pageSize, + totalElements = totalElements, + totalPages = totalPages, + isFirst = pageNumber == 0, + isLast = pageNumber == totalPages - 1, + hasNext = pageNumber < totalPages - 1, + hasPrevious = pageNumber > 0 + ) + } + } +} + +/** + * ์บ์‹œ ํ†ต๊ณ„๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class CacheStatistics( + val hitCount: Long, + val missCount: Long, + val putCount: Long, + val evictionCount: Long, + val hitRate: Double, + val missRate: Double, + val estimatedSize: Long +) { + fun getTotalRequestCount(): Long = hitCount + missCount +} + +/** + * ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์˜ค๋ฅ˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” sealed class์ž…๋‹ˆ๋‹ค. + */ +sealed class RepositoryError( + val message: String, + val cause: Throwable? = null +) { + companion object { + const val MSG_NOT_FOUND = "์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค" + const val MSG_CONNECTION_ERROR = "์—ฐ๊ฒฐ ์˜ค๋ฅ˜" + const val MSG_DATA_INTEGRITY_ERROR = "๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ์˜ค๋ฅ˜" + const val MSG_CONCURRENCY_ERROR = "๋™์‹œ์„ฑ ์˜ค๋ฅ˜" + const val MSG_PERMISSION_ERROR = "๊ถŒํ•œ ์˜ค๋ฅ˜" + const val MSG_VALIDATION_ERROR = "๊ฒ€์ฆ ์˜ค๋ฅ˜" + const val MSG_UNKNOWN_ERROR = "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜" + } + + /** + * ์ง‘ํ•ฉ ๋ฃจํŠธ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class NotFound(val id: Any) : RepositoryError("${MSG_NOT_FOUND}: $id") + + /** + * ์—ฐ๊ฒฐ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class ConnectionError(val reason: String, val throwable: Throwable? = null) : + RepositoryError("${MSG_CONNECTION_ERROR}: $reason", throwable) + + /** + * ๋ฐ์ดํ„ฐ ๋ฌด๊ฒฐ์„ฑ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class DataIntegrityError(val reason: String, val throwable: Throwable? = null) : + RepositoryError("${MSG_DATA_INTEGRITY_ERROR}: $reason", throwable) + + /** + * ๋™์‹œ์„ฑ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class ConcurrencyError(val reason: String, val throwable: Throwable? = null) : + RepositoryError("${MSG_CONCURRENCY_ERROR}: $reason", throwable) + + /** + * ๊ถŒํ•œ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class PermissionError(val reason: String) : RepositoryError("${MSG_PERMISSION_ERROR}: $reason") + + /** + * ๊ฒ€์ฆ ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class ValidationError(val violations: List) : + RepositoryError("${MSG_VALIDATION_ERROR}: ${violations.joinToString(", ")}") + + /** + * ์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜์ž…๋‹ˆ๋‹ค. + */ + data class UnknownError(val reason: String, val throwable: Throwable? = null) : + RepositoryError("${MSG_UNKNOWN_ERROR}: $reason", throwable) +} + +/** + * ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ํŒฉํ† ๋ฆฌ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + */ +interface RepositoryFactory { + + /** + * ์ง€์ •๋œ ํƒ€์ž…์˜ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateType ์ง‘ํ•ฉ ๋ฃจํŠธ ํƒ€์ž… + * @return ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค + */ + fun createRepository(aggregateType: Class): Repository + + /** + * ์ฝ๊ธฐ ์ „์šฉ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateType ์ง‘ํ•ฉ ๋ฃจํŠธ ํƒ€์ž… + * @return ์ฝ๊ธฐ ์ „์šฉ ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค + */ + fun createReadOnlyRepository(aggregateType: Class): ReadOnlyRepository + + /** + * ์บ์‹œ ์ง€์› ๋ฆฌํฌ์ง€ํ† ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param aggregateType ์ง‘ํ•ฉ ๋ฃจํŠธ ํƒ€์ž… + * @return ์บ์‹œ ์ง€์› ๋ฆฌํฌ์ง€ํ† ๋ฆฌ ์ธ์Šคํ„ด์Šค + */ + fun createCacheableRepository(aggregateType: Class): CacheableRepository +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ValidationResult.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ValidationResult.kt new file mode 100644 index 00000000..c65a1ad4 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ValidationResult.kt @@ -0,0 +1,17 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ๊ฒ€์ฆ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๋ฐ์ดํ„ฐ ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +data class ValidationResult( + val isValid: Boolean, + val errors: List = emptyList(), + val warnings: List = emptyList() +) { + companion object { + fun success() = ValidationResult(true) + fun failure(errors: List) = ValidationResult(false, errors) + fun failureWithWarnings(errors: List, warnings: List) = + ValidationResult(false, errors, warnings) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ValidationStrategy.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ValidationStrategy.kt new file mode 100644 index 00000000..69d8c094 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ValidationStrategy.kt @@ -0,0 +1,26 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ๊ฒ€์ฆ ์ „๋žต์„ ์œ„ํ•œ ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋‹ค์–‘ํ•œ ๊ฒ€์ฆ ๋กœ์ง์„ ์ถ”์ƒํ™”ํ•ฉ๋‹ˆ๋‹ค. + * + * @param T ๊ฒ€์ฆํ•  ๊ฐ์ฒด์˜ ํƒ€์ž… + */ +interface ValidationStrategy { + + /** + * ๊ฐ์ฒด๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * + * @param target ๊ฒ€์ฆํ•  ๊ฐ์ฒด + * @return ๊ฒ€์ฆ ๊ฒฐ๊ณผ + */ + fun validate(target: T): ValidationResult + + /** + * ๊ฒ€์ฆ ์ „๋žต์˜ ์šฐ์„ ์ˆœ์œ„๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์šฐ์„ ์ˆœ์œ„ (๋‚ฎ์€ ์ˆซ์ž๊ฐ€ ๋†’์€ ์šฐ์„ ์ˆœ์œ„) + */ + fun getPriority(): Int = Int.MAX_VALUE +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ValueObject.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ValueObject.kt new file mode 100644 index 00000000..dca294b5 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/interfaces/ValueObject.kt @@ -0,0 +1,26 @@ +package hs.kr.entrydsm.global.interfaces + +/** + * ๊ฐ’ ๊ฐ์ฒด(Value Object)๋ฅผ ์œ„ํ•œ ๋งˆ์ปค ์ธํ„ฐํŽ˜์ด์Šค์ž…๋‹ˆ๋‹ค. + * + * ๋ถˆ๋ณ€์„ฑ๊ณผ ๋™๋“ฑ์„ฑ ๊ธฐ๋ฐ˜ ๋น„๊ต๋ฅผ ๋ณด์žฅํ•˜๋Š” ๊ฐ’ ๊ฐ์ฒด์ž„์„ ๋ช…์‹œํ•ฉ๋‹ˆ๋‹ค. + */ +interface ValueObject : DomainObject { + + /** + * ๊ฐ’ ๊ฐ์ฒด์˜ ํ•ด์‹œ ์ฝ”๋“œ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * ๋ชจ๋“  ์†์„ฑ์„ ๊ธฐ๋ฐ˜์œผ๋กœ ๊ณ„์‚ฐ๋˜์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ํ•ด์‹œ ์ฝ”๋“œ + */ + override fun hashCode(): Int + + /** + * ๊ฐ’ ๊ฐ์ฒด์˜ ๋™๋“ฑ์„ฑ์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * ๋ชจ๋“  ์†์„ฑ์ด ๊ฐ™์•„์•ผ ๋™๋“ฑํ•œ ๊ฒƒ์œผ๋กœ ํŒ๋‹จํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ตํ•  ๊ฐ์ฒด + * @return ๋™๋“ฑํ•˜๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + override fun equals(other: Any?): Boolean +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/values/Position.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/values/Position.kt new file mode 100644 index 00000000..2665cf13 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/values/Position.kt @@ -0,0 +1,169 @@ +package hs.kr.entrydsm.global.values + +/** + * ํ…์ŠคํŠธ ๋‚ด์—์„œ์˜ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * ์ž…๋ ฅ ํ…์ŠคํŠธ์—์„œ ํŠน์ • ๋ฌธ์ž๋‚˜ ํ† ํฐ์˜ ์œ„์น˜๋ฅผ ์ถ”์ ํ•˜๊ธฐ ์œ„ํ•ด ์‚ฌ์šฉ๋˜๋ฉฐ, + * ์˜ค๋ฅ˜ ๋ณด๊ณ  ์‹œ ์ •ํ™•ํ•œ ์œ„์น˜ ์ •๋ณด๋ฅผ ์ œ๊ณตํ•˜๋Š” ๋ฐ ํ™œ์šฉ๋ฉ๋‹ˆ๋‹ค. + * ๋ถˆ๋ณ€ ๊ฐ์ฒด๋กœ ์„ค๊ณ„๋˜์–ด ์•ˆ์ „ํ•œ ๊ฐ’ ์ „๋‹ฌ์„ ๋ณด์žฅํ•ฉ๋‹ˆ๋‹ค. + * + * @property index 0๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ๋ฌธ์ž ์ธ๋ฑ์Šค + * @property line 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ์ค„ ๋ฒˆํ˜ธ + * @property column 1๋ถ€ํ„ฐ ์‹œ์ž‘ํ•˜๋Š” ์ปฌ๋Ÿผ ๋ฒˆํ˜ธ + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.15 + */ +data class Position( + val index: Int, + val line: Int, + val column: Int +) { + + init { + require(index >= MIN_INDEX) { "$MSG_INVALID_INDEX: $index" } + require(line >= MIN_LINE) { "$MSG_INVALID_LINE: $line" } + require(column >= MIN_COLUMN) { "$MSG_INVALID_COLUMN: $column" } + } + + companion object { + + // ๊ธฐ๋ณธ๊ฐ’ ์ƒ์ˆ˜ + const val MIN_INDEX = 0 + const val MIN_LINE = 1 + const val MIN_COLUMN = 1 + const val DEFAULT_LINE = 1 + const val LINE_INCREMENT = 1 + const val COLUMN_RESET = 1 + const val COLUMN_INCREMENT = 1 + const val INDEX_INCREMENT = 1 + const val MIN_COUNT = 0 + + // ๋ฌธ์ž ์ƒ์ˆ˜ + const val NEWLINE_CHAR = '\n' + + // ์—๋Ÿฌ ๋ฉ”์‹œ์ง€ ์ƒ์ˆ˜ + const val MSG_INVALID_INDEX = "์ธ๋ฑ์Šค๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val MSG_INVALID_LINE = "์ค„ ๋ฒˆํ˜ธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val MSG_INVALID_COLUMN = "์ปฌ๋Ÿผ ๋ฒˆํ˜ธ๋Š” 1 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val MSG_INVALID_COUNT = "์ด๋™ ๊ฐœ์ˆ˜๋Š” 0 ์ด์ƒ์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค" + const val MSG_INDEX_OUT_OF_BOUNDS = "์ธ๋ฑ์Šค๊ฐ€ ํ…์ŠคํŠธ ๊ธธ์ด๋ฅผ ์ดˆ๊ณผํ•ฉ๋‹ˆ๋‹ค" + + /** + * ์‹œ์ž‘ ์œ„์น˜๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ์ƒ์ˆ˜์ž…๋‹ˆ๋‹ค. + */ + val START = Position(MIN_INDEX, MIN_LINE, MIN_COLUMN) + + /** + * ์ธ๋ฑ์Šค๋งŒ์œผ๋กœ ์œ„์น˜๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * ์ค„ ๋ฒˆํ˜ธ์™€ ์ปฌ๋Ÿผ ๋ฒˆํ˜ธ๋Š” ๊ณ„์‚ฐ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค. + * + * @param index ๋ฌธ์ž ์ธ๋ฑ์Šค + * @return Position ์ธ์Šคํ„ด์Šค + */ + fun of(index: Int): Position = Position(index, DEFAULT_LINE, index + MIN_COLUMN) + + /** + * ํ…์ŠคํŠธ์™€ ์ธ๋ฑ์Šค๋ฅผ ๊ธฐ๋ฐ˜์œผ๋กœ ์ •ํ™•ํ•œ ์œ„์น˜๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param text ์ „์ฒด ํ…์ŠคํŠธ + * @param index ๋Œ€์ƒ ์ธ๋ฑ์Šค + * @return ๊ณ„์‚ฐ๋œ Position ์ธ์Šคํ„ด์Šค + */ + fun calculate(text: String, index: Int): Position { + require(index >= MIN_INDEX) { "$MSG_INVALID_INDEX: $index" } + require(index <= text.length) { "$MSG_INDEX_OUT_OF_BOUNDS: $index > ${text.length}" } + + var line = MIN_LINE + var column = MIN_COLUMN + + for (i in MIN_INDEX until index) { + if (text[i] == NEWLINE_CHAR) { + line += LINE_INCREMENT + column = COLUMN_RESET + } else { + column += COLUMN_INCREMENT + } + } + + return Position(index, line, column) + } + } + + /** + * ๋‹ค์Œ ๋ฌธ์ž ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param isNewLine ํ˜„์žฌ ๋ฌธ์ž๊ฐ€ ๊ฐœํ–‰ ๋ฌธ์ž์ธ์ง€ ์—ฌ๋ถ€ + * @return ๋‹ค์Œ ์œ„์น˜์˜ Position ์ธ์Šคํ„ด์Šค + */ + fun next(isNewLine: Boolean = false): Position = if (isNewLine) { + Position(index + INDEX_INCREMENT, line + LINE_INCREMENT, COLUMN_RESET) + } else { + Position(index + INDEX_INCREMENT, line, column + COLUMN_INCREMENT) + } + + /** + * ์ง€์ •๋œ ๊ฐœ์ˆ˜๋งŒํผ ์•ž์œผ๋กœ ์ด๋™ํ•œ ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param count ์ด๋™ํ•  ๋ฌธ์ž ๊ฐœ์ˆ˜ + * @return ์ด๋™๋œ Position ์ธ์Šคํ„ด์Šค + */ + fun advance(count: Int): Position { + require(count >= MIN_COUNT) { "$MSG_INVALID_COUNT: $count" } + return Position(index + count, line, column + count) + } + + /** + * ๋‹ค์Œ ์ค„๋กœ ์ด๋™ํ•œ ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹ค์Œ ์ค„์˜ ์ฒซ ๋ฒˆ์งธ ์ปฌ๋Ÿผ Position ์ธ์Šคํ„ด์Šค + */ + fun nextLine(): Position = Position(index + INDEX_INCREMENT, line + LINE_INCREMENT, COLUMN_RESET) + + /** + * ๋‹ค์Œ ์ปฌ๋Ÿผ์œผ๋กœ ์ด๋™ํ•œ ์œ„์น˜๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ๋‹ค์Œ ์ปฌ๋Ÿผ Position ์ธ์Šคํ„ด์Šค + */ + fun nextColumn(): Position = Position(index + INDEX_INCREMENT, line, column + COLUMN_INCREMENT) + + /** + * ํŠน์ • ์œ„์น˜๊นŒ์ง€์˜ ๊ฑฐ๋ฆฌ๋ฅผ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋Œ€์ƒ ์œ„์น˜ + * @return ๋ฌธ์ž ๊ฐœ์ˆ˜ ๊ธฐ์ค€ ๊ฑฐ๋ฆฌ + */ + fun distanceTo(other: Position): Int = kotlin.math.abs(other.index - this.index) + + /** + * ํŠน์ • ์œ„์น˜ ์ด์ „์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ต ๋Œ€์ƒ ์œ„์น˜ + * @return ์ด์ „ ์œ„์น˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isBefore(other: Position): Boolean = this.index < other.index + + /** + * ํŠน์ • ์œ„์น˜ ์ดํ›„์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๋น„๊ต ๋Œ€์ƒ ์œ„์น˜ + * @return ์ดํ›„ ์œ„์น˜์ด๋ฉด true, ์•„๋‹ˆ๋ฉด false + */ + fun isAfter(other: Position): Boolean = this.index > other.index + + /** + * ์œ„์น˜ ์ •๋ณด๋ฅผ ๋ฌธ์ž์—ด๋กœ ํ‘œํ˜„ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "line:column (index)" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + override fun toString(): String = "$line:$column ($index)" + + /** + * ๊ฐ„๋‹จํ•œ ํ˜•ํƒœ์˜ ์œ„์น˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return "line:column" ํ˜•ํƒœ์˜ ๋ฌธ์ž์—ด + */ + fun toShortString(): String = "$line:$column" +} \ No newline at end of file diff --git a/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/values/Result.kt b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/values/Result.kt new file mode 100644 index 00000000..89bf2b18 --- /dev/null +++ b/casper-application-domain/src/main/kotlin/hs/kr/entrydsm/global/values/Result.kt @@ -0,0 +1,335 @@ +package hs.kr.entrydsm.global.values + +/** + * ๋„๋ฉ”์ธ ์—ฐ์‚ฐ์˜ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ๊ฐ’ ๊ฐ์ฒด์ž…๋‹ˆ๋‹ค. + * + * DDD Value Object ํŒจํ„ด์„ ์ ์šฉํ•˜์—ฌ ์„ฑ๊ณต๊ณผ ์‹คํŒจ๋ฅผ ๋ช…์‹œ์ ์œผ๋กœ ํ‘œํ˜„ํ•˜๋ฉฐ, + * ํ•จ์ˆ˜ํ˜• ํ”„๋กœ๊ทธ๋ž˜๋ฐ์˜ Result ํƒ€์ž…๊ณผ ์œ ์‚ฌํ•œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•ฉ๋‹ˆ๋‹ค. + * ์˜ˆ์™ธ ๋Œ€์‹  ๋ช…์‹œ์ ์ธ ๊ฒฐ๊ณผ ํƒ€์ž…์„ ์‚ฌ์šฉํ•˜์—ฌ ์•ˆ์ „ํ•˜๊ณ  ์˜ˆ์ธก ๊ฐ€๋Šฅํ•œ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. + * + * @param T ์„ฑ๊ณต ์‹œ ๋ฐ˜ํ™˜๋˜๋Š” ๊ฐ’์˜ ํƒ€์ž… + * @param E ์‹คํŒจ ์‹œ ๋ฐ˜ํ™˜๋˜๋Š” ์˜ค๋ฅ˜์˜ ํƒ€์ž… + * + * @see ์ฝ”๋“œ ์‚ฌ๋ก€๋กœ ๋ณด๋Š” Domain-Driven ํ—ฅ์‚ฌ๊ณ ๋‚  ์•„ํ‚คํ…์ฒ˜ + * + * @author kangeunchan + * @since 2025.07.20 + */ +sealed class Result { + + /** + * ์„ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @param value ์„ฑ๊ณต ์‹œ ๋ฐ˜ํ™˜๋˜๋Š” ๊ฐ’ + */ + data class Success(val value: T) : Result() { + override fun toString(): String = "Success($value)" + } + + /** + * ์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ๋‚˜ํƒ€๋‚ด๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + * + * @param error ์‹คํŒจ ์‹œ ๋ฐ˜ํ™˜๋˜๋Š” ์˜ค๋ฅ˜ + */ + data class Failure(val error: E) : Result() { + override fun toString(): String = "Failure($error)" + } + + /** + * ๊ฒฐ๊ณผ๊ฐ€ ์„ฑ๊ณต์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณต์ด๋ฉด true, ์‹คํŒจ๋ฉด false + */ + fun isSuccess(): Boolean = this is Success + + /** + * ๊ฒฐ๊ณผ๊ฐ€ ์‹คํŒจ์ธ์ง€ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹คํŒจ๋ฉด true, ์„ฑ๊ณต์ด๋ฉด false + */ + fun isFailure(): Boolean = this is Failure + + /** + * ์„ฑ๊ณต ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ null์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณต ์‹œ ๊ฐ’, ์‹คํŒจ ์‹œ null + */ + fun getOrNull(): T? = when (this) { + is Success -> value + is Failure -> null + } + + /** + * ์‹คํŒจ ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ null์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์‹คํŒจ ์‹œ ์˜ค๋ฅ˜, ์„ฑ๊ณต ์‹œ null + */ + fun errorOrNull(): E? = when (this) { + is Success -> null + is Failure -> error + } + + /** + * ์„ฑ๊ณต ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ๊ธฐ๋ณธ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param defaultValue ๊ธฐ๋ณธ๊ฐ’ + * @return ์„ฑ๊ณต ์‹œ ๊ฐ’, ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ๊ฐ’ + */ + fun getOrDefault(defaultValue: @UnsafeVariance T): T = when (this) { + is Success -> value + is Failure -> defaultValue + } + + /** + * ์„ฑ๊ณต ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ๋žŒ๋‹ค ํ•จ์ˆ˜์˜ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param onFailure ์‹คํŒจ ์‹œ ์‹คํ–‰ํ•  ๋žŒ๋‹ค ํ•จ์ˆ˜ + * @return ์„ฑ๊ณต ์‹œ ๊ฐ’, ์‹คํŒจ ์‹œ ๋žŒ๋‹ค ํ•จ์ˆ˜ ๊ฒฐ๊ณผ + */ + inline fun getOrElse(onFailure: (E) -> @UnsafeVariance T): T = when (this) { + is Success -> value + is Failure -> onFailure(error) + } + + /** + * ์„ฑ๊ณต ๊ฐ’์„ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param transform ๋ณ€ํ™˜ ํ•จ์ˆ˜ + * @return ๋ณ€ํ™˜๋œ ๊ฒฐ๊ณผ + */ + inline fun map(transform: (T) -> R): Result = when (this) { + is Success -> Success(transform(value)) + is Failure -> this + } + + /** + * ์‹คํŒจ ์˜ค๋ฅ˜๋ฅผ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param transform ๋ณ€ํ™˜ ํ•จ์ˆ˜ + * @return ๋ณ€ํ™˜๋œ ๊ฒฐ๊ณผ + */ + inline fun mapError(transform: (E) -> F): Result = when (this) { + is Success -> this + is Failure -> Failure(transform(error)) + } + + /** + * ์„ฑ๊ณต ๊ฐ’์— ๋‹ค๋ฅธ Result๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ์ ์šฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param transform ๋ณ€ํ™˜ ํ•จ์ˆ˜ + * @return ํ‰๋ฉดํ™”๋œ ๊ฒฐ๊ณผ + */ + inline fun flatMap(transform: (T) -> Result): Result = when (this) { + is Success -> transform(value) + is Failure -> this + } + + /** + * ์„ฑ๊ณต ์‹œ ์ง€์ •๋œ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param action ์ˆ˜ํ–‰ํ•  ๋™์ž‘ + * @return ํ˜„์žฌ Result ์ธ์Šคํ„ด์Šค + */ + inline fun onSuccess(action: (T) -> Unit): Result { + if (this is Success) action(value) + return this + } + + /** + * ์‹คํŒจ ์‹œ ์ง€์ •๋œ ๋™์ž‘์„ ์ˆ˜ํ–‰ํ•ฉ๋‹ˆ๋‹ค. + * + * @param action ์ˆ˜ํ–‰ํ•  ๋™์ž‘ + * @return ํ˜„์žฌ Result ์ธ์Šคํ„ด์Šค + */ + inline fun onFailure(action: (E) -> Unit): Result { + if (this is Failure) action(error) + return this + } + + /** + * ๊ฒฐ๊ณผ๋ฅผ ํด๋“œํ•ฉ๋‹ˆ๋‹ค. + * + * @param onSuccess ์„ฑ๊ณต ์‹œ ๋ณ€ํ™˜ ํ•จ์ˆ˜ + * @param onFailure ์‹คํŒจ ์‹œ ๋ณ€ํ™˜ ํ•จ์ˆ˜ + * @return ํด๋“œ๋œ ๊ฒฐ๊ณผ + */ + inline fun fold( + onSuccess: (T) -> R, + onFailure: (E) -> R + ): R = when (this) { + is Success -> onSuccess(value) + is Failure -> onFailure(error) + } + + /** + * ์„ฑ๊ณต ๊ฐ’์— ๋Œ€ํ•ด ์กฐ๊ฑด์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. + * + * @param predicate ํ™•์ธํ•  ์กฐ๊ฑด + * @param errorProvider ์กฐ๊ฑด์ด false์ผ ๋•Œ ๋ฐ˜ํ™˜ํ•  ์˜ค๋ฅ˜ ์ œ๊ณต์ž + * @return ์กฐ๊ฑด์ด ์ฐธ์ด๋ฉด ํ˜„์žฌ ๊ฒฐ๊ณผ, ๊ฑฐ์ง“์ด๋ฉด ์‹คํŒจ ๊ฒฐ๊ณผ + */ + inline fun filter( + predicate: (T) -> Boolean, + errorProvider: () -> F + ): Result = when (this) { + is Success -> if (predicate(value)) Success(value) else Failure(errorProvider()) + is Failure -> Failure(errorProvider()) + } + + /** + * ๋‹ค๋ฅธ Result์™€ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param other ๊ฒฐํ•ฉํ•  ๋‹ค๋ฅธ Result + * @param combiner ๊ฒฐํ•ฉ ํ•จ์ˆ˜ + * @return ๊ฒฐํ•ฉ๋œ ๊ฒฐ๊ณผ + */ + inline fun zip( + other: Result, + combiner: (T, U) -> R + ): Result = when { + this is Success && other is Success -> Success(combiner(this.value, other.value)) + this is Failure -> this + other is Failure -> other + else -> throw IllegalStateException("Unreachable") + } + + /** + * Result๋ฅผ List๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณต ์‹œ ๊ฐ’์„ ํฌํ•จํ•œ ๋ฆฌ์ŠคํŠธ, ์‹คํŒจ ์‹œ ๋นˆ ๋ฆฌ์ŠคํŠธ + */ + fun toList(): List = when (this) { + is Success -> listOf(value) + is Failure -> emptyList() + } + + /** + * ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉฐ ์„ฑ๊ณต ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @return ์„ฑ๊ณต ์‹œ ๊ฐ’ + * @throws IllegalStateException ์‹คํŒจ ์‹œ + */ + fun getOrThrow(): T = when (this) { + is Success -> value + is Failure -> throw IllegalStateException("Result is failure: $error") + } + + /** + * ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋ฉฐ ์„ฑ๊ณต ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param exceptionProvider ์˜ˆ์™ธ ์ œ๊ณต์ž + * @return ์„ฑ๊ณต ์‹œ ๊ฐ’ + * @throws Exception ์‹คํŒจ ์‹œ ์ œ๊ณต๋œ ์˜ˆ์™ธ + */ + inline fun getOrThrow(exceptionProvider: (E) -> Exception): T = when (this) { + is Success -> value + is Failure -> throw exceptionProvider(error) + } + + companion object { + /** + * ์„ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param value ์„ฑ๊ณต ๊ฐ’ + * @return Success ์ธ์Šคํ„ด์Šค + */ + fun success(value: T): Result = Success(value) + + /** + * ์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param error ์‹คํŒจ ์˜ค๋ฅ˜ + * @return Failure ์ธ์Šคํ„ด์Šค + */ + fun failure(error: E): Result = Failure(error) + + /** + * ์˜ˆ์™ธ๋ฅผ ํฌ์ฐฉํ•˜์—ฌ Result๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param block ์‹คํ–‰ํ•  ์ฝ”๋“œ ๋ธ”๋ก + * @return ์„ฑ๊ณต ๋˜๋Š” ์˜ˆ์™ธ ๊ฒฐ๊ณผ + */ + inline fun catch(block: () -> T): Result = try { + Success(block()) + } catch (e: Exception) { + Failure(e) + } + + /** + * ์กฐ๊ฑด์— ๋”ฐ๋ผ Result๋ฅผ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. + * + * @param condition ์กฐ๊ฑด + * @param value ์„ฑ๊ณต ๊ฐ’ ์ œ๊ณต์ž + * @param error ์‹คํŒจ ์˜ค๋ฅ˜ ์ œ๊ณต์ž + * @return ์กฐ๊ฑด์— ๋”ฐ๋ฅธ Result + */ + inline fun of( + condition: Boolean, + value: () -> T, + error: () -> E + ): Result = if (condition) Success(value()) else Failure(error()) + + /** + * nullable ๊ฐ’์„ Result๋กœ ๋ณ€ํ™˜ํ•ฉ๋‹ˆ๋‹ค. + * + * @param value nullable ๊ฐ’ + * @param error null์ผ ๋•Œ ๋ฐ˜ํ™˜ํ•  ์˜ค๋ฅ˜ + * @return ๊ฐ’์ด ์žˆ์œผ๋ฉด Success, null์ด๋ฉด Failure + */ + fun fromNullable(value: T?, error: E): Result = + if (value != null) Success(value) else Failure(error) + + /** + * Result ๋ฆฌ์ŠคํŠธ๋ฅผ ํ•˜๋‚˜์˜ Result๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param results Result ๋ฆฌ์ŠคํŠธ + * @return ๋ชจ๋“  ๊ฒฐ๊ณผ๊ฐ€ ์„ฑ๊ณต์ด๋ฉด ์„ฑ๊ณต ๋ฆฌ์ŠคํŠธ, ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจ๋ฉด ์ฒซ ๋ฒˆ์งธ ์‹คํŒจ + */ + fun combine(results: List>): Result, E> { + val values = mutableListOf() + for (result in results) { + when (result) { + is Success -> values.add(result.value) + is Failure -> return result + } + } + return Success(values) + } + + /** + * Result ์‹œํ€€์Šค๋ฅผ ํ•˜๋‚˜์˜ Result๋กœ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + * + * @param results Result ์‹œํ€€์Šค + * @return ๋ชจ๋“  ๊ฒฐ๊ณผ๊ฐ€ ์„ฑ๊ณต์ด๋ฉด ์„ฑ๊ณต ๋ฆฌ์ŠคํŠธ, ํ•˜๋‚˜๋ผ๋„ ์‹คํŒจ๋ฉด ์ฒซ ๋ฒˆ์งธ ์‹คํŒจ + */ + fun combineSequence(results: Sequence>): Result, E> = + combine(results.toList()) + } +} + +/** + * Result ํ™•์žฅ ํ•จ์ˆ˜๋“ค + */ + +/** + * ๋‘ Result๋ฅผ ๊ฒฐํ•ฉํ•ฉ๋‹ˆ๋‹ค. + */ +infix fun Result.and(other: Result): Result, E> = + zip(other) { a, b -> a to b } + +/** + * Result ์ฒด์ด๋‹์„ ์œ„ํ•œ ํ™•์žฅ ํ•จ์ˆ˜ + */ +infix fun Result.then(next: (T) -> Result): Result = flatMap(next) + +/** + * ์กฐ๊ฑด๋ถ€ Result ๋ณ€ํ™˜ + */ +inline fun Result.takeIf(predicate: (T) -> Boolean): Result = + map { value -> value.takeIf(predicate) } + +/** + * ์กฐ๊ฑด๋ถ€ Result ๋ณ€ํ™˜ (๋ถ€์ •) + */ +inline fun Result.takeUnless(predicate: (T) -> Boolean): Result = + map { value -> value.takeUnless(predicate) } \ No newline at end of file diff --git a/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/ast/factories/ASTNodeFactoryTest.kt b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/ast/factories/ASTNodeFactoryTest.kt new file mode 100644 index 00000000..fe0b7099 --- /dev/null +++ b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/ast/factories/ASTNodeFactoryTest.kt @@ -0,0 +1,219 @@ +package hs.kr.entrydsm.domain.ast.factories + +import hs.kr.entrydsm.domain.ast.entities.* +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeEach +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertNotNull + +/** + * ASTNodeFactory์˜ ์‹ค์ œ ๊ตฌํ˜„๋œ ๋ฉ”์„œ๋“œ๋“ค๋งŒ ํ…Œ์ŠคํŠธํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +class ASTNodeFactoryTest { + + private lateinit var factory: ASTNodeFactory + + @BeforeEach + fun setUp() { + factory = ASTNodeFactory() + } + + @Test + fun `createNumber๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ˆซ์ž ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val node = factory.createNumber(42.0) + assertEquals(42.0, node.value) + assertTrue(node is NumberNode) + } + + @Test + fun `createBoolean์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ถˆ๋ฆฌ์–ธ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val trueNode = factory.createBoolean(true) + assertTrue(trueNode.value) + assertTrue(trueNode is BooleanNode) + + val falseNode = factory.createBoolean(false) + assertEquals(false, falseNode.value) + } + + @Test + fun `createVariable์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋ณ€์ˆ˜ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val varNode = factory.createVariable("x") + assertEquals("x", varNode.name) + assertTrue(varNode is VariableNode) + } + + @Test + fun `createBinaryOp์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ดํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val left = factory.createNumber(10.0) + val right = factory.createNumber(5.0) + + val addNode = factory.createBinaryOp(left, "+", right) + assertEquals("+", addNode.operator) + assertEquals(left, addNode.left) + assertEquals(right, addNode.right) + assertTrue(addNode is BinaryOpNode) + } + + @Test + fun `createUnaryOp์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋‹จํ•ญ ์—ฐ์‚ฐ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val operand = factory.createNumber(42.0) + + val negNode = factory.createUnaryOp("-", operand) + assertEquals("-", negNode.operator) + assertEquals(operand, negNode.operand) + assertTrue(negNode is UnaryOpNode) + } + + @Test + fun `createFunctionCall์ด ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ํ•จ์ˆ˜ ํ˜ธ์ถœ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val arg1 = factory.createNumber(1.0) + val arg2 = factory.createNumber(2.0) + + val maxNode = factory.createFunctionCall("max", listOf(arg1, arg2)) + assertEquals("max", maxNode.name) + assertEquals(2, maxNode.args.size) + assertEquals(arg1, maxNode.args[0]) + assertEquals(arg2, maxNode.args[1]) + assertTrue(maxNode is FunctionCallNode) + } + + @Test + fun `createIf๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์กฐ๊ฑด๋ฌธ ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val condition = factory.createBinaryOp( + factory.createVariable("x"), + ">", + factory.createNumber(0.0) + ) + val trueValue = factory.createNumber(1.0) + val falseValue = factory.createNumber(-1.0) + + val ifNode = factory.createIf(condition, trueValue, falseValue) + + assertEquals(condition, ifNode.condition) + assertEquals(trueValue, ifNode.trueValue) + assertEquals(falseValue, ifNode.falseValue) + assertTrue(ifNode is IfNode) + } + + @Test + fun `createArguments๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ธ์ˆ˜ ๋ชฉ๋ก ๋…ธ๋“œ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val arg1 = factory.createNumber(1.0) + val arg2 = factory.createVariable("x") + val arg3 = factory.createBoolean(true) + + val argsNode = factory.createArguments(listOf(arg1, arg2, arg3)) + + assertEquals(3, argsNode.arguments.size) + assertEquals(arg1, argsNode.arguments[0]) + assertEquals(arg2, argsNode.arguments[1]) + assertEquals(arg3, argsNode.arguments[2]) + assertTrue(argsNode is ArgumentsNode) + } + + @Test + fun `๋ณต์žกํ•œ ํ‘œํ˜„์‹ ํŠธ๋ฆฌ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + // (x + 2) * (y - 1) ํ‘œํ˜„์‹ ์ƒ์„ฑ + val x = factory.createVariable("x") + val two = factory.createNumber(2.0) + val y = factory.createVariable("y") + val one = factory.createNumber(1.0) + + val leftExpr = factory.createBinaryOp(x, "+", two) + val rightExpr = factory.createBinaryOp(y, "-", one) + val result = factory.createBinaryOp(leftExpr, "*", rightExpr) + + assertEquals("*", result.operator) + assertTrue(result.left is BinaryOpNode) + assertTrue(result.right is BinaryOpNode) + + val leftBinary = result.left as BinaryOpNode + assertEquals("+", leftBinary.operator) + assertEquals("x", (leftBinary.left as VariableNode).name) + assertEquals(2.0, (leftBinary.right as NumberNode).value) + + val rightBinary = result.right as BinaryOpNode + assertEquals("-", rightBinary.operator) + assertEquals("y", (rightBinary.left as VariableNode).name) + assertEquals(1.0, (rightBinary.right as NumberNode).value) + } + + @Test + fun `์ค‘์ฒฉ๋œ ํ•จ์ˆ˜ ํ˜ธ์ถœ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + // max(min(a, b), c) ๊ฐ™์€ ์ค‘์ฒฉ๋œ ํ•จ์ˆ˜ ํ˜ธ์ถœ + val a = factory.createVariable("a") + val b = factory.createVariable("b") + val c = factory.createVariable("c") + + val minCall = factory.createFunctionCall("min", listOf(a, b)) + val maxCall = factory.createFunctionCall("max", listOf(minCall, c)) + + assertEquals("max", maxCall.name) + assertEquals(2, maxCall.args.size) + assertTrue(maxCall.args[0] is FunctionCallNode) + + val innerCall = maxCall.args[0] as FunctionCallNode + assertEquals("min", innerCall.name) + assertEquals(2, innerCall.args.size) + assertEquals("a", (innerCall.args[0] as VariableNode).name) + assertEquals("b", (innerCall.args[1] as VariableNode).name) + + assertEquals("c", (maxCall.args[1] as VariableNode).name) + } + + @Test + fun `๋…ผ๋ฆฌ ์—ฐ์‚ฐ์ž๋ฅผ ์‚ฌ์šฉํ•œ ํ‘œํ˜„์‹์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + // (x > 0) && (y < 10) ํ‘œํ˜„์‹ ์ƒ์„ฑ + val x = factory.createVariable("x") + val zero = factory.createNumber(0.0) + val y = factory.createVariable("y") + val ten = factory.createNumber(10.0) + + val leftCondition = factory.createBinaryOp(x, ">", zero) + val rightCondition = factory.createBinaryOp(y, "<", ten) + val andExpression = factory.createBinaryOp(leftCondition, "&&", rightCondition) + + assertEquals("&&", andExpression.operator) + assertTrue(andExpression.left is BinaryOpNode) + assertTrue(andExpression.right is BinaryOpNode) + + val leftBinary = andExpression.left as BinaryOpNode + assertEquals(">", leftBinary.operator) + assertEquals("x", (leftBinary.left as VariableNode).name) + assertEquals(0.0, (leftBinary.right as NumberNode).value) + + val rightBinary = andExpression.right as BinaryOpNode + assertEquals("<", rightBinary.operator) + assertEquals("y", (rightBinary.left as VariableNode).name) + assertEquals(10.0, (rightBinary.right as NumberNode).value) + } + + @Test + fun `์‚ผํ•ญ ์—ฐ์‚ฐ์ž(์กฐ๊ฑด๋ฌธ)๋ฅผ ์‚ฌ์šฉํ•œ ํ‘œํ˜„์‹์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + // x > 0 ? x : 0 ํ‘œํ˜„์‹ ์ƒ์„ฑ + val x = factory.createVariable("x") + val zero = factory.createNumber(0.0) + val condition = factory.createBinaryOp(x, ">", zero) + val trueValue = factory.createVariable("x") + val falseValue = factory.createNumber(0.0) + + val ternary = factory.createIf(condition, trueValue, falseValue) + + assertTrue(ternary.condition is BinaryOpNode) + assertTrue(ternary.trueValue is VariableNode) + assertTrue(ternary.falseValue is NumberNode) + + val conditionBinary = ternary.condition as BinaryOpNode + assertEquals(">", conditionBinary.operator) + assertEquals("x", (conditionBinary.left as VariableNode).name) + assertEquals(0.0, (conditionBinary.right as NumberNode).value) + + assertEquals("x", (ternary.trueValue as VariableNode).name) + assertEquals(0.0, (ternary.falseValue as NumberNode).value) + } + + @Test + fun `ํŒฉํ† ๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ธ์Šคํ„ด์Šคํ™”๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + assertNotNull(factory) + } +} \ No newline at end of file diff --git a/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/CalculatorFunctionalTest.kt b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/CalculatorFunctionalTest.kt new file mode 100644 index 00000000..70229abd --- /dev/null +++ b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/CalculatorFunctionalTest.kt @@ -0,0 +1,275 @@ +package hs.kr.entrydsm.domain.calculator + +import hs.kr.entrydsm.domain.calculator.aggregates.Calculator +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertNotNull + +/** + * ๊ณ„์‚ฐ๊ธฐ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ + * + * ์ ์ˆ˜ ๊ณ„์‚ฐ์— ์‚ฌ์šฉ๋˜๋Š” ๊ธฐ๋ณธ์ ์ธ ์‚ฐ์ˆ  ์—ฐ์‚ฐ๊ณผ ํ•จ์ˆ˜๋“ค์ด + * ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ +@DisplayName("๊ณ„์‚ฐ๊ธฐ ๊ธฐ๋ณธ ๊ธฐ๋Šฅ ๊ฒ€์ฆ ํ…Œ์ŠคํŠธ") +class CalculatorFunctionalTest { + + private lateinit var calculator: Calculator + + @BeforeEach + fun setUp() { + calculator = Calculator.createDefault() + } + + @Test + @DisplayName("๊ธฐ๋ณธ ์‚ฐ์ˆ  ์—ฐ์‚ฐ ํ…Œ์ŠคํŠธ") + fun `๊ธฐ๋ณธ ์‚ฐ์ˆ  ์—ฐ์‚ฐ์ด ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ`() { + val testCases = mapOf( + "2 + 3" to 5.0, + "10 - 4" to 6.0, + "3 * 4" to 12.0, + "15 / 3" to 5.0, + "2 + 3 * 4" to 14.0, // ์—ฐ์‚ฐ์ž ์šฐ์„ ์ˆœ์œ„ + "(2 + 3) * 4" to 20.0, // ๊ด„ํ˜ธ ์šฐ์„ ์ˆœ์œ„ + "7 / 2" to 3.5 // ์†Œ์ˆ˜์  ๋‚˜๋ˆ„๊ธฐ + ) + + testCases.forEach { (formula, expected) -> + val request = CalculationRequest(formula) + val result = calculator.calculate(request) + + assertTrue(result.isSuccess(), "Formula '$formula' failed: ${result.errors}") + assertEquals(expected, result.result as Double, 0.001, "Formula '$formula' result mismatch") + println("โœ“ $formula = ${result.result}") + } + } + + @Test + @DisplayName("๋ณ€์ˆ˜๋ฅผ ์‚ฌ์šฉํ•œ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ") + fun `๋ณ€์ˆ˜๊ฐ€ ํฌํ•จ๋œ ์ˆ˜์‹ ๊ณ„์‚ฐ์ด ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ`() { + val variables = mapOf( + "x" to 5, + "y" to 3, + "korean" to 4, + "math" to 5, + "english" to 3 + ) + + val testCases = mapOf( + "x + y" to 8.0, + "x * y" to 15.0, + "x / y" to (5.0/3.0), + "(korean + math + english) / 3" to 4.0, // ํ‰๊ท  ๊ณ„์‚ฐ + "korean * 2 + math * 3" to 23.0 // ๊ฐ€์ค‘ ๊ณ„์‚ฐ + ) + + testCases.forEach { (formula, expected) -> + val request = CalculationRequest(formula, variables) + val result = calculator.calculate(request) + + assertTrue(result.isSuccess(), "Formula '$formula' failed: ${result.errors}") + assertEquals(expected, result.result as Double, 0.001, "Formula '$formula' result mismatch") + println("โœ“ $formula = ${result.result}") + } + } + + @Test + @DisplayName("IF ์กฐ๊ฑด๋ฌธ ํ…Œ์ŠคํŠธ") + fun `IF ์กฐ๊ฑด๋ฌธ์ด ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ`() { + val variables = mapOf( + "score" to 85, + "absent_days" to 2, + "volunteer_hours" to 18 + ) + + val testCases = mapOf( + "IF(score > 80, 1, 0)" to 1.0, // true ๊ฒฝ์šฐ + "IF(score < 70, 1, 0)" to 0.0, // false ๊ฒฝ์šฐ + "IF(absent_days < 5, 15, 10)" to 15.0, // ์ถœ์„์ ์ˆ˜ ๊ณ„์‚ฐ + "IF(volunteer_hours > 15, 15, volunteer_hours)" to 15.0 // MIN ํ•จ์ˆ˜ ๋Œ€์ฒด + ) + + testCases.forEach { (formula, expected) -> + val request = CalculationRequest(formula, variables) + val result = calculator.calculate(request) + + assertTrue(result.isSuccess(), "Formula '$formula' failed: ${result.errors}") + assertEquals(expected, result.result as Double, 0.001, "Formula '$formula' result mismatch") + println("โœ“ $formula = ${result.result}") + } + } + + @Test + @DisplayName("์ค‘์ฒฉ๋œ IF ์กฐ๊ฑด๋ฌธ ํ…Œ์ŠคํŠธ") + fun `์ค‘์ฒฉ๋œ IF ์กฐ๊ฑด๋ฌธ์ด ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ`() { + val variables = mapOf("days" to 3) + + // ์ถœ์„์ ์ˆ˜ ๊ณ„์‚ฐ๊ณผ ์œ ์‚ฌํ•œ ์ค‘์ฒฉ IF๋ฌธ + val formula = "IF(days >= 5, 10, IF(days >= 3, 12, IF(days >= 1, 14, 15)))" + val request = CalculationRequest(formula, variables) + val result = calculator.calculate(request) + + assertTrue(result.isSuccess(), "Nested IF failed: ${result.errors}") + assertEquals(12.0, result.result as Double, 0.001) + println("โœ“ Nested IF: $formula = ${result.result}") + + // ๋‹ค๋ฅธ ๊ฐ’์œผ๋กœ ํ…Œ์ŠคํŠธ + val testCases = mapOf( + 0 to 15.0, // days < 1 + 1 to 14.0, // 1 <= days < 3 + 3 to 12.0, // 3 <= days < 5 + 5 to 10.0 // days >= 5 + ) + + testCases.forEach { (daysValue, expected) -> + val vars = mapOf("days" to daysValue) + val req = CalculationRequest(formula, vars) + val res = calculator.calculate(req) + + assertTrue(res.isSuccess()) + assertEquals(expected, res.result as Double, 0.001) + println("โœ“ days=$daysValue -> ${res.result}") + } + } + + @Test + @DisplayName("๋ณต์žกํ•œ ์ ์ˆ˜ ๊ณ„์‚ฐ ์ˆ˜์‹ ํ…Œ์ŠคํŠธ") + fun `์‹ค์ œ ์ ์ˆ˜ ๊ณ„์‚ฐ๊ณผ ์œ ์‚ฌํ•œ ๋ณต์žกํ•œ ์ˆ˜์‹์ด ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ`() { + val variables = mapOf( + "k1" to 4, "s1" to 3, "h1" to 4, "m1" to 5, "sc1" to 4, "t1" to 3, "e1" to 4, // 3-1 + "k2" to 3, "s2" to 4, "h2" to 3, "m2" to 4, "sc2" to 3, "t2" to 4, "e2" to 3, // 2-2 + "k3" to 4, "s3" to 4, "h3" to 5, "m3" to 4, "sc3" to 3, "t3" to 4, "e3" to 4 // 2-1 + ) + + // ์‹ค์ œ ๊ต๊ณผ์ ์ˆ˜ ๊ณ„์‚ฐ๊ณผ ๋™์ผํ•œ ๋ณต์žกํ•œ ์ˆ˜์‹ + val formula = """ + (8 * ((k1 + s1 + h1 + m1 + sc1 + t1 + e1) / 7) + + 4 * ((k2 + s2 + h2 + m2 + sc2 + t2 + e2) / 7) + + 4 * ((k3 + s3 + h3 + m3 + sc3 + t3 + e3) / 7)) * 1.75 + """.trimIndent() + + val request = CalculationRequest(formula, variables) + val result = calculator.calculate(request) + + assertTrue(result.isSuccess(), "Complex formula failed: ${result.errors}") + assertNotNull(result.result) + + // ๋‹จ๊ณ„๋ณ„ ๊ณ„์‚ฐ์œผ๋กœ ๊ฒ€์ฆ + val avg1 = (4+3+4+5+4+3+4) / 7.0 // 27/7 + val avg2 = (3+4+3+4+3+4+3) / 7.0 // 24/7 + val avg3 = (4+4+5+4+3+4+4) / 7.0 // 28/7 + val expected = (8 * avg1 + 4 * avg2 + 4 * avg3) * 1.75 + + assertEquals(expected, result.result as Double, 0.001) + println("โœ“ Complex formula result: ${result.result} (expected: $expected)") + } + + @Test + @DisplayName("์˜ค๋ฅ˜ ์ƒํ™ฉ ์ฒ˜๋ฆฌ ํ…Œ์ŠคํŠธ") + fun `๋‹ค์–‘ํ•œ ์˜ค๋ฅ˜ ์ƒํ™ฉ์—์„œ ์ ์ ˆํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๊ฐ€ ๋˜๋Š”์ง€ ํ™•์ธ`() { + val errorCases = listOf( + "5 / 0", // 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ + "undefined_var + 1", // ์ •์˜๋˜์ง€ ์•Š์€ ๋ณ€์ˆ˜ + "5 +", // ๋ฌธ๋ฒ• ์˜ค๋ฅ˜ + "(((5 + 3)", // ๊ด„ํ˜ธ ๋ถˆ์ผ์น˜ + "" // ๋นˆ ์ˆ˜์‹ + ) + + errorCases.forEach { formula -> + try { + val request = CalculationRequest(formula) + val result = calculator.calculate(request) + + if (!result.isSuccess()) { + println("โœ“ Expected error for '$formula': ${result.errors}") + } else { + println("? Unexpected success for '$formula': ${result.result}") + } + } catch (e: Exception) { + println("โœ“ Expected exception for '$formula': ${e.message}") + } + } + } + + @Test + @DisplayName("์„ฑ๋Šฅ ํ…Œ์ŠคํŠธ") + fun `๊ณ„์‚ฐ ์„ฑ๋Šฅ์ด ํ•ฉ๋ฆฌ์ ์ธ ๋ฒ”์œ„ ๋‚ด์— ์žˆ๋Š”์ง€ ํ™•์ธ`() { + val variables = mapOf( + "a" to 1, "b" to 2, "c" to 3, "d" to 4, "e" to 5 + ) + + val complexFormula = "((a + b) * (c + d) - e) / ((a * b) + (c * d))" + + // 100๋ฒˆ ๋ฐ˜๋ณต ์‹คํ–‰ + val startTime = System.currentTimeMillis() + repeat(100) { + val request = CalculationRequest(complexFormula, variables) + val result = calculator.calculate(request) + assertTrue(result.isSuccess()) + } + val endTime = System.currentTimeMillis() + + val totalTime = endTime - startTime + val avgTime = totalTime / 100.0 + + println("โœ“ Performance test: 100 calculations in ${totalTime}ms (avg: ${avgTime}ms)") + assertTrue(avgTime < 100.0, "Average calculation time too slow: ${avgTime}ms") + } + + @Test + @DisplayName("์‹ค์ œ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋กœ ์ข…ํ•ฉ ํ…Œ์ŠคํŠธ") + fun `์‹ค์ œ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ์‚ฌ์šฉํ•œ ์ข…ํ•ฉ ์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ`() { + val userData = mapOf( + "korean_3_1" to 4, "social_3_1" to 3, "history_3_1" to 4, "math_3_1" to 5, + "science_3_1" to 4, "tech_3_1" to 3, "english_3_1" to 4, + "korean_2_2" to 3, "social_2_2" to 4, "history_2_2" to 3, "math_2_2" to 4, + "science_2_2" to 3, "tech_2_2" to 4, "english_2_2" to 3, + "korean_2_1" to 4, "social_2_1" to 4, "history_2_1" to 5, "math_2_1" to 4, + "science_2_1" to 3, "tech_2_1" to 4, "english_2_1" to 4, + "absent_days" to 0, "late_count" to 1, "volunteer_hours" to 18 + ) + + // ๋‹จ๊ณ„๋ณ„ ๊ณ„์‚ฐ + val formulas = listOf( + "(korean_3_1 + social_3_1 + history_3_1 + math_3_1 + science_3_1 + tech_3_1 + english_3_1) / 7", + "(korean_2_2 + social_2_2 + history_2_2 + math_2_2 + science_2_2 + tech_2_2 + english_2_2) / 7", + "(korean_2_1 + social_2_1 + history_2_1 + math_2_1 + science_2_1 + tech_2_1 + english_2_1) / 7", + "__entry_calc_step_1 * 8 + __entry_calc_step_2 * 4 + __entry_calc_step_3 * 4", // ๊ธฐ์ค€์ ์ˆ˜ + "__entry_calc_step_4 * 1.75", // ๊ต๊ณผ์ ์ˆ˜ + "IF(volunteer_hours > 15, 15, volunteer_hours)", // ๋ด‰์‚ฌ์ ์ˆ˜ + "__entry_calc_step_5 + __entry_calc_step_6 + 15" // ์ด์  (์ถœ์„์ ์ˆ˜ 15์  ๊ฐ€์ •) + ) + + val results = try { + calculator.calculateMultiStep(formulas, userData) + } catch (e: Exception) { + println("Exception during calculation: ${e.message}") + e.printStackTrace() + throw e + } + + assertEquals(7, results.size) + results.forEach { result -> + assertTrue(result.isSuccess(), "Calculation failed: ${result.errors}") + assertNotNull(result.result) + } + + val finalScore = results.last().result as Double + println("=== ์‹ค์ œ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ===") + println("3ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท : ${String.format("%.3f", results[0].result)}") + println("2ํ•™๋…„ 2ํ•™๊ธฐ ํ‰๊ท : ${String.format("%.3f", results[1].result)}") + println("2ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท : ${String.format("%.3f", results[2].result)}") + println("๊ต๊ณผ ๊ธฐ์ค€์ ์ˆ˜: ${String.format("%.3f", results[3].result)}์ ") + println("๊ต๊ณผ์ ์ˆ˜: ${String.format("%.3f", results[4].result)}์ ") + println("๋ด‰์‚ฌ์ ์ˆ˜: ${(results[5].result as Double).toInt()}์ ") + println("์ด์ : ${String.format("%.3f", finalScore)}์ ") + println("===============================") + + // ํ•ฉ๋ฆฌ์ ์ธ ์ ์ˆ˜ ๋ฒ”์œ„ ํ™•์ธ + assertTrue(finalScore > 80.0 && finalScore < 173.0, "Total score out of reasonable range: $finalScore") + } +} \ No newline at end of file diff --git a/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/MultiStepScoreCalculationTest.kt b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/MultiStepScoreCalculationTest.kt new file mode 100644 index 00000000..3d3015e7 --- /dev/null +++ b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/MultiStepScoreCalculationTest.kt @@ -0,0 +1,248 @@ +package hs.kr.entrydsm.domain.calculator + +import hs.kr.entrydsm.domain.calculator.aggregates.Calculator +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertNotNull + +/** + * ๋Œ€๋•์†Œํ”„ํŠธ์›จ์–ด๋งˆ์ด์Šคํ„ฐ๊ณ ๋“ฑํ•™๊ต ํŠน๋ณ„์ „ํ˜• ๋‹ค๋‹จ๊ณ„ ์ ์ˆ˜ ๊ณ„์‚ฐ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ + * + * ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ JSON๊ณผ ๋™์ผํ•œ ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ํ”„๋กœ์„ธ์Šค๋ฅผ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + */ +@DisplayName("๋Œ€๋•์†Œํ”„ํŠธ์›จ์–ด๋งˆ์ด์Šคํ„ฐ๊ณ ๋“ฑํ•™๊ต ํŠน๋ณ„์ „ํ˜• ๋‹ค๋‹จ๊ณ„ ์ ์ˆ˜ ๊ณ„์‚ฐ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") +class MultiStepScoreCalculationTest { + + private lateinit var calculator: Calculator + + // ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ๋ณ€์ˆ˜๋“ค + private val variables = mapOf( + "korean_3_1" to 4, "social_3_1" to 3, "history_3_1" to 4, "math_3_1" to 5, + "science_3_1" to 4, "tech_3_1" to 3, "english_3_1" to 4, + "korean_2_2" to 3, "social_2_2" to 4, "history_2_2" to 3, "math_2_2" to 4, + "science_2_2" to 3, "tech_2_2" to 4, "english_2_2" to 3, + "korean_2_1" to 4, "social_2_1" to 4, "history_2_1" to 5, "math_2_1" to 4, + "science_2_1" to 3, "tech_2_1" to 4, "english_2_1" to 4, + "absent_days" to 0, "late_count" to 1, "early_leave_count" to 0, "lesson_absence_count" to 0, + "volunteer_hours" to 18, "algorithm_award" to 0, "info_license" to 0 + ) + + @BeforeEach + fun setUp() { + calculator = Calculator.createDefault() + } + + @Test + @DisplayName("์ „์ฒด ๋‹ค๋‹จ๊ณ„ ์ ์ˆ˜ ๊ณ„์‚ฐ ํ”„๋กœ์„ธ์Šค ํ…Œ์ŠคํŠธ") + fun `์‚ฌ์šฉ์ž ์ œ๊ณต JSON๊ณผ ๋™์ผํ•œ ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ์ด ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ`() { + + // ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ ์ •ํ™•ํ•œ ๋‹จ๊ณ„๋ณ„ ์ˆ˜์‹๋“ค + val formulas = listOf( + // 1. 3ํ•™๋…„ 1ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท  + "(korean_3_1 + social_3_1 + history_3_1 + math_3_1 + science_3_1 + tech_3_1 + english_3_1) / 7", + + // 2. 2ํ•™๋…„ 2ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท  + "(korean_2_2 + social_2_2 + history_2_2 + math_2_2 + science_2_2 + tech_2_2 + english_2_2) / 7", + + // 3. 2ํ•™๋…„ 1ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท  + "(korean_2_1 + social_2_1 + history_2_1 + math_2_1 + science_2_1 + tech_2_1 + english_2_1) / 7", + + // 4. 3ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ (40์  ๋งŒ์ ) - __entry_calc_step_1 ์‚ฌ์šฉ + "8 * __entry_calc_step_1", + + // 5. 2ํ•™๋…„ 2ํ•™๊ธฐ ์ ์ˆ˜ (20์  ๋งŒ์ ) - __entry_calc_step_2 ์‚ฌ์šฉ + "4 * __entry_calc_step_2", + + // 6. 2ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ (20์  ๋งŒ์ ) - __entry_calc_step_3 ์‚ฌ์šฉ + "4 * __entry_calc_step_3", + + // 7. ๊ต๊ณผ ๊ธฐ์ค€์ ์ˆ˜ (80์  ๋งŒ์ ) - __entry_calc_step_4, __entry_calc_step_5, __entry_calc_step_6 ์‚ฌ์šฉ + "__entry_calc_step_4 + __entry_calc_step_5 + __entry_calc_step_6", + + // 8. ์ผ๋ฐ˜์ „ํ˜• ๊ต๊ณผ์ ์ˆ˜ (140์  ๋งŒ์ ) - __entry_calc_step_7 ์‚ฌ์šฉ + "__entry_calc_step_7 * 1.75", + + // 9. ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜ ๊ณ„์‚ฐ + "absent_days + late_count/3 + early_leave_count/3 + lesson_absence_count/3", + + // 10. ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜ ์ •์ˆ˜๋ณ€ํ™˜ (ROUND ํ•จ์ˆ˜ ๋Œ€์‹  ๊ฐ„๋‹จํ•œ ์ฒ˜๋ฆฌ) + "__entry_calc_step_9", // ์‹ค์ œ๋กœ๋Š” ROUND(__entry_calc_step_9 - 0.5, 0)์ด์ง€๋งŒ ๋‹จ์ˆœํ™” + + // 11. ์ถœ์„์ ์ˆ˜ (๋ณต์žกํ•œ IF๋ฌธ ๋Œ€์‹  ๋‹จ์ˆœํ™”) - ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜๊ฐ€ 1๋ฏธ๋งŒ์ด๋ฉด 15์  + "IF(__entry_calc_step_10 < 1, 15, 14)", + + // 12. ๋ด‰์‚ฌํ™œ๋™์ ์ˆ˜ (MIN ํ•จ์ˆ˜ ๋Œ€์‹  IF๋ฌธ) + "IF(volunteer_hours > 15, 15, volunteer_hours)", + + // 13. ๊ฐ€์‚ฐ์  + "algorithm_award * 3 + info_license * 0", + + // 14. ์ตœ์ข… ์ด์  ๊ณ„์‚ฐ + "__entry_calc_step_8 + __entry_calc_step_11 + __entry_calc_step_12 + __entry_calc_step_13" + ) + + // ๋‹ค๋‹จ๊ณ„ ๊ณ„์‚ฐ ์‹คํ–‰ + val results = try { + calculator.calculateMultiStep(formulas, variables) + } catch (e: Exception) { + println("Exception during calculation: ${e.message}") + e.printStackTrace() + throw e + } + + // ๋ชจ๋“  ๋‹จ๊ณ„๊ฐ€ ์„ฑ๊ณตํ–ˆ๋Š”์ง€ ํ™•์ธ + assertEquals(14, results.size) + results.forEachIndexed { index, result -> + assertTrue(result.isSuccess(), "Step ${index + 1} failed: ${result.errors}") + assertNotNull(result.result, "Step ${index + 1} result is null") + } + + // ๊ฐ ๋‹จ๊ณ„๋ณ„ ๊ฒฐ๊ณผ ๊ฒ€์ฆ + val step1Result = results[0].result as Double // 3ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท  + val step2Result = results[1].result as Double // 2ํ•™๋…„ 2ํ•™๊ธฐ ํ‰๊ท  + val step3Result = results[2].result as Double // 2ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท  + val step4Result = results[3].result as Double // 3ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ + val step5Result = results[4].result as Double // 2ํ•™๋…„ 2ํ•™๊ธฐ ์ ์ˆ˜ + val step6Result = results[5].result as Double // 2ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ + val step7Result = results[6].result as Double // ๊ต๊ณผ ๊ธฐ์ค€์ ์ˆ˜ + val step8Result = results[7].result as Double // ์ผ๋ฐ˜์ „ํ˜• ๊ต๊ณผ์ ์ˆ˜ + val step9Result = results[8].result as Double // ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜ + val step10Result = results[9].result as Double // ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜ ์ •์ˆ˜๋ณ€ํ™˜ + val step11Result = results[10].result as Double // ์ถœ์„์ ์ˆ˜ + val step12Result = results[11].result as Double // ๋ด‰์‚ฌ์ ์ˆ˜ + val step13Result = results[12].result as Double // ๊ฐ€์‚ฐ์  + val finalResult = results[13].result as Double // ์ตœ์ข… ์ ์ˆ˜ + + // ์˜ˆ์ƒ๊ฐ’ ๊ณ„์‚ฐ ๋ฐ ๊ฒ€์ฆ + + // Step 1: (4+3+4+5+4+3+4)/7 = 27/7 โ‰ˆ 3.857 + assertEquals(27.0/7.0, step1Result, 0.001, "3ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท  ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 2: (3+4+3+4+3+4+3)/7 = 24/7 โ‰ˆ 3.429 + assertEquals(24.0/7.0, step2Result, 0.001, "2ํ•™๋…„ 2ํ•™๊ธฐ ํ‰๊ท  ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 3: (4+4+5+4+3+4+4)/7 = 28/7 = 4.0 + assertEquals(4.0, step3Result, 0.001, "2ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท  ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 4: 8 * step1 = 8 * (27/7) โ‰ˆ 30.857 + assertEquals(8.0 * 27.0/7.0, step4Result, 0.001, "3ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 5: 4 * step2 = 4 * (24/7) โ‰ˆ 13.714 + assertEquals(4.0 * 24.0/7.0, step5Result, 0.001, "2ํ•™๋…„ 2ํ•™๊ธฐ ์ ์ˆ˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 6: 4 * step3 = 4 * 4 = 16.0 + assertEquals(16.0, step6Result, 0.001, "2ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 7: step4 + step5 + step6 โ‰ˆ 30.857 + 13.714 + 16 = 60.571 + val expectedStep7 = 8.0 * 27.0/7.0 + 4.0 * 24.0/7.0 + 16.0 + assertEquals(expectedStep7, step7Result, 0.001, "๊ต๊ณผ ๊ธฐ์ค€์ ์ˆ˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 8: step7 * 1.75 โ‰ˆ 60.571 * 1.75 โ‰ˆ 106.0 + assertEquals(expectedStep7 * 1.75, step8Result, 0.001, "์ผ๋ฐ˜์ „ํ˜• ๊ต๊ณผ์ ์ˆ˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 9: 0 + 1/3 + 0/3 + 0/3 = 1/3 โ‰ˆ 0.333 + assertEquals(1.0/3.0, step9Result, 0.001, "ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 10: step9 (๋‹จ์ˆœํ™”) โ‰ˆ 0.333 + assertEquals(step9Result, step10Result, 0.001, "ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜ ์ •์ˆ˜๋ณ€ํ™˜ ์˜ค๋ฅ˜") + + // Step 11: IF(0.333 < 1, 15, 14) = 15 + assertEquals(15.0, step11Result, 0.001, "์ถœ์„์ ์ˆ˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 12: IF(18 > 15, 15, 18) = 15 + assertEquals(15.0, step12Result, 0.001, "๋ด‰์‚ฌ์ ์ˆ˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 13: 0 * 3 + 0 * 0 = 0 + assertEquals(0.0, step13Result, 0.001, "๊ฐ€์‚ฐ์  ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // Step 14: step8 + step11 + step12 + step13 โ‰ˆ 106.0 + 15 + 15 + 0 = 136.0 + val expectedFinal = expectedStep7 * 1.75 + 15.0 + 15.0 + 0.0 + assertEquals(expectedFinal, finalResult, 0.001, "์ตœ์ข… ์ ์ˆ˜ ๊ณ„์‚ฐ ์˜ค๋ฅ˜") + + // ๊ฒฐ๊ณผ ์ถœ๋ ฅ + println("=== ๋‹ค๋‹จ๊ณ„ ์ ์ˆ˜ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ===") + println("Step 1 - 3ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท : ${String.format("%.3f", step1Result)}") + println("Step 2 - 2ํ•™๋…„ 2ํ•™๊ธฐ ํ‰๊ท : ${String.format("%.3f", step2Result)}") + println("Step 3 - 2ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท : ${String.format("%.3f", step3Result)}") + println("Step 4 - 3ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜: ${String.format("%.3f", step4Result)}์ ") + println("Step 5 - 2ํ•™๋…„ 2ํ•™๊ธฐ ์ ์ˆ˜: ${String.format("%.3f", step5Result)}์ ") + println("Step 6 - 2ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜: ${String.format("%.3f", step6Result)}์ ") + println("Step 7 - ๊ต๊ณผ ๊ธฐ์ค€์ ์ˆ˜: ${String.format("%.3f", step7Result)}์  (80์  ๋งŒ์ )") + println("Step 8 - ์ผ๋ฐ˜์ „ํ˜• ๊ต๊ณผ์ ์ˆ˜: ${String.format("%.3f", step8Result)}์  (140์  ๋งŒ์ )") + println("Step 9 - ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜: ${String.format("%.3f", step9Result)}์ผ") + println("Step 10 - ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜ ์ •์ˆ˜๋ณ€ํ™˜: ${String.format("%.3f", step10Result)}์ผ") + println("Step 11 - ์ถœ์„์ ์ˆ˜: ${(step11Result as Number).toInt()}์  (15์  ๋งŒ์ )") + println("Step 12 - ๋ด‰์‚ฌ์ ์ˆ˜: ${(step12Result as Number).toInt()}์  (15์  ๋งŒ์ )") + println("Step 13 - ๊ฐ€์‚ฐ์ : ${(step13Result as Number).toInt()}์  (3์  ๋งŒ์ )") + println("Step 14 - ์ตœ์ข… ์ด์ : ${String.format("%.3f", finalResult)}์  (173์  ๋งŒ์ )") + println("=============================") + } + + @Test + @DisplayName("์‹ค์ œ JSON ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ์™€ ์ •ํ™•ํžˆ ๋งค์นญ๋˜๋Š” ํ…Œ์ŠคํŠธ") + fun `์‚ฌ์šฉ์ž JSON์˜ ๊ฐ ๋‹จ๊ณ„๋ช…๊ณผ ์ˆ˜์‹์ด ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ`() { + // ์‚ฌ์šฉ์ž๊ฐ€ ์ œ๊ณตํ•œ JSON์˜ steps ๊ตฌ์กฐ๋ฅผ ๊ทธ๋Œ€๋กœ ํ…Œ์ŠคํŠธ + val steps = listOf( + Triple("3ํ•™๋…„ 1ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท ", "(korean_3_1 + social_3_1 + history_3_1 + math_3_1 + science_3_1 + tech_3_1 + english_3_1) / 7", "semester_3_1_avg"), + Triple("2ํ•™๋…„ 2ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท ", "(korean_2_2 + social_2_2 + history_2_2 + math_2_2 + science_2_2 + tech_2_2 + english_2_2) / 7", "semester_2_2_avg"), + Triple("2ํ•™๋…„ 1ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท ", "(korean_2_1 + social_2_1 + history_2_1 + math_2_1 + science_2_1 + tech_2_1 + english_2_1) / 7", "semester_2_1_avg"), + Triple("3ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ (40์  ๋งŒ์ )", "8 * __entry_calc_step_1", "score_3_1"), + Triple("2ํ•™๋…„ 2ํ•™๊ธฐ ์ ์ˆ˜ (20์  ๋งŒ์ )", "4 * __entry_calc_step_2", "score_2_2"), + Triple("2ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ (20์  ๋งŒ์ )", "4 * __entry_calc_step_3", "score_2_1"), + Triple("๊ต๊ณผ ๊ธฐ์ค€์ ์ˆ˜ (80์  ๋งŒ์ )", "__entry_calc_step_4 + __entry_calc_step_5 + __entry_calc_step_6", "base_academic_score"), + Triple("์ผ๋ฐ˜์ „ํ˜• ๊ต๊ณผ์ ์ˆ˜ (140์  ๋งŒ์ )", "__entry_calc_step_7 * 1.75", "academic_score") + ) + + val formulas = steps.map { it.second } + val results = try { + calculator.calculateMultiStep(formulas, variables) + } catch (e: Exception) { + println("Exception during calculation: ${e.message}") + e.printStackTrace() + throw e + } + + // ๊ฐ ๋‹จ๊ณ„๋ณ„ ๊ฒ€์ฆ + steps.forEachIndexed { index, (stepName, formula, resultVariable) -> + val result = results[index] + assertTrue(result.isSuccess(), "$stepName ๊ณ„์‚ฐ ์‹คํŒจ: ${result.errors}") + assertNotNull(result.result, "$stepName ๊ฒฐ๊ณผ๊ฐ€ null์ž…๋‹ˆ๋‹ค") + + println("โœ“ $stepName: ${String.format("%.3f", result.result)} ($resultVariable)") + } + + // ์ฃผ์š” ๊ฒ€์ฆ ํฌ์ธํŠธ๋“ค + assertEquals(27.0/7.0, results[0].result as Double, 0.001) // 3ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท  + assertEquals(24.0/7.0, results[1].result as Double, 0.001) // 2ํ•™๋…„ 2ํ•™๊ธฐ ํ‰๊ท  + assertEquals(4.0, results[2].result as Double, 0.001) // 2ํ•™๋…„ 1ํ•™๊ธฐ ํ‰๊ท  + + val academicScore = results[7].result as Double + assertTrue(academicScore > 100.0 && academicScore < 110.0, "๊ต๊ณผ์ ์ˆ˜๊ฐ€ ์˜ˆ์ƒ ๋ฒ”์œ„๋ฅผ ๋ฒ—์–ด๋‚จ: $academicScore") + } + + @Test + @DisplayName("์ˆ˜์‹ ์˜ค๋ฅ˜ ์ƒํ™ฉ ํ…Œ์ŠคํŠธ") + fun `์ž˜๋ชป๋œ ์ˆ˜์‹์ด๋‚˜ ๋ณ€์ˆ˜ ์ฐธ์กฐ ์‹œ ์ ์ ˆํ•œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๊ฐ€ ๋˜๋Š”์ง€ ํ™•์ธ`() { + val invalidFormulas = listOf( + "nonexistent_variable + 1", // ์กด์žฌํ•˜์ง€ ์•Š๋Š” ๋ณ€์ˆ˜ + "korean_3_1 / 0", // 0์œผ๋กœ ๋‚˜๋ˆ„๊ธฐ + "__entry_calc_step_10", // ์กด์žฌํ•˜์ง€ ์•Š๋Š” step ์ฐธ์กฐ + "korean_3_1 + " // ๋ฌธ๋ฒ• ์˜ค๋ฅ˜ + ) + + invalidFormulas.forEach { formula -> + try { + val results = calculator.calculateMultiStep(listOf(formula), variables) + // ์ผ๋ถ€ ์˜ค๋ฅ˜๋Š” ๊ณ„์‚ฐ ๊ณผ์ •์—์„œ ์žกํž ์ˆ˜ ์žˆ์Œ + if (results.isNotEmpty() && !results[0].isSuccess()) { + println("โœ“ ์˜ˆ์ƒ๋œ ์˜ค๋ฅ˜ ์ฒ˜๋ฆฌ๋จ: $formula -> ${results[0].errors}") + } + } catch (e: Exception) { + println("โœ“ ์˜ˆ์ƒ๋œ ์˜ˆ์™ธ ๋ฐœ์ƒ: $formula -> ${e.message}") + } + } + } +} \ No newline at end of file diff --git a/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/ScoreCalculationTest.kt b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/ScoreCalculationTest.kt new file mode 100644 index 00000000..f2b4d07d --- /dev/null +++ b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/ScoreCalculationTest.kt @@ -0,0 +1,377 @@ +package hs.kr.entrydsm.domain.calculator + +import hs.kr.entrydsm.domain.calculator.aggregates.Calculator +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertNotNull + +/** + * ๋Œ€๋•์†Œํ”„ํŠธ์›จ์–ด๋งˆ์ด์Šคํ„ฐ๊ณ ๋“ฑํ•™๊ต ํŠน๋ณ„์ „ํ˜• ์ ์ˆ˜ ๊ณ„์‚ฐ ์ˆ˜์‹ ํ…Œ์ŠคํŠธ + * + * ์‹ค์ œ ์ž…์‹œ ์ ์ˆ˜ ๊ณ„์‚ฐ ๋กœ์ง์ด ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜๋Š”์ง€ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค. + * ๊ต๊ณผ์ ์ˆ˜(140์ ) + ์ถœ์„์ ์ˆ˜(15์ ) + ๋ด‰์‚ฌ์ ์ˆ˜(15์ ) + ๊ฐ€์‚ฐ์ (3์ ) = ์ด 173์  ๋งŒ์  + */ +@DisplayName("๋Œ€๋•์†Œํ”„ํŠธ์›จ์–ด๋งˆ์ด์Šคํ„ฐ๊ณ ๋“ฑํ•™๊ต ํŠน๋ณ„์ „ํ˜• ์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ") +class ScoreCalculationTest { + + private lateinit var calculator: Calculator + + // ํ…Œ์ŠคํŠธ์šฉ ๋ณ€์ˆ˜๋“ค (์‹ค์ œ ํ•™์ƒ ์„ฑ์  ๋ฐ์ดํ„ฐ) + private val variables = mapOf( + // 3ํ•™๋…„ 1ํ•™๊ธฐ ์„ฑ์  + "korean_3_1" to 4, + "social_3_1" to 3, + "history_3_1" to 4, + "math_3_1" to 5, + "science_3_1" to 4, + "tech_3_1" to 3, + "english_3_1" to 4, + + // 2ํ•™๋…„ 2ํ•™๊ธฐ ์„ฑ์  + "korean_2_2" to 3, + "social_2_2" to 4, + "history_2_2" to 3, + "math_2_2" to 4, + "science_2_2" to 3, + "tech_2_2" to 4, + "english_2_2" to 3, + + // 2ํ•™๋…„ 1ํ•™๊ธฐ ์„ฑ์  + "korean_2_1" to 4, + "social_2_1" to 4, + "history_2_1" to 5, + "math_2_1" to 4, + "science_2_1" to 3, + "tech_2_1" to 4, + "english_2_1" to 4, + + // ์ถœ๊ฒฐ ์ •๋ณด + "absent_days" to 0, + "late_count" to 1, + "early_leave_count" to 0, + "lesson_absence_count" to 0, + + // ๊ธฐํƒ€ ์ •๋ณด + "volunteer_hours" to 18, + "algorithm_award" to 0, + "info_license" to 0 + ) + + @BeforeEach + fun setUp() { + calculator = Calculator.createDefault() + } + + @Test + @DisplayName("3ํ•™๋…„ 1ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท  ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ") + fun `3ํ•™๋…„ 1ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท ์ด ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ํ™•์ธ`() { + val formula = "(korean_3_1 + social_3_1 + history_3_1 + math_3_1 + science_3_1 + tech_3_1 + english_3_1) / 7" + val request = CalculationRequest(formula, variables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + assertNotNull(result.result) + + // ์˜ˆ์ƒ๊ฐ’: (4+3+4+5+4+3+4) / 7 = 27/7 โ‰ˆ 3.857 + val expected = 27.0 / 7.0 + assertEquals(expected, result.result as Double, 0.001) + } + + @Test + @DisplayName("2ํ•™๋…„ 2ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท  ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ") + fun `2ํ•™๋…„ 2ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท ์ด ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ํ™•์ธ`() { + val formula = "(korean_2_2 + social_2_2 + history_2_2 + math_2_2 + science_2_2 + tech_2_2 + english_2_2) / 7" + val request = CalculationRequest(formula, variables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + // ์˜ˆ์ƒ๊ฐ’: (3+4+3+4+3+4+3) / 7 = 24/7 โ‰ˆ 3.429 + val expected = 24.0 / 7.0 + assertEquals(expected, result.result as Double, 0.001) + } + + @Test + @DisplayName("2ํ•™๋…„ 1ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท  ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ") + fun `2ํ•™๋…„ 1ํ•™๊ธฐ ๊ต๊ณผํ‰๊ท ์ด ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ํ™•์ธ`() { + val formula = "(korean_2_1 + social_2_1 + history_2_1 + math_2_1 + science_2_1 + tech_2_1 + english_2_1) / 7" + val request = CalculationRequest(formula, variables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + // ์˜ˆ์ƒ๊ฐ’: (4+4+5+4+3+4+4) / 7 = 28/7 = 4.0 + assertEquals(4.0, result.result as Double, 0.001) + } + + @Test + @DisplayName("ํ•™๊ธฐ๋ณ„ ์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ") + fun `ํ•™๊ธฐ๋ณ„ ์ ์ˆ˜๊ฐ€ ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ํ™•์ธ`() { + // 3ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ (40์  ๋งŒ์ ) + val formula_3_1 = "8 * ((korean_3_1 + social_3_1 + history_3_1 + math_3_1 + science_3_1 + tech_3_1 + english_3_1) / 7)" + val request_3_1 = CalculationRequest(formula_3_1, variables) + val result_3_1 = calculator.calculate(request_3_1) + + assertTrue(result_3_1.isSuccess()) + // ์˜ˆ์ƒ๊ฐ’: 8 * (27/7) โ‰ˆ 30.857 + assertEquals(8.0 * 27.0 / 7.0, result_3_1.result as Double, 0.001) + + // 2ํ•™๋…„ 2ํ•™๊ธฐ ์ ์ˆ˜ (20์  ๋งŒ์ ) + val formula_2_2 = "4 * ((korean_2_2 + social_2_2 + history_2_2 + math_2_2 + science_2_2 + tech_2_2 + english_2_2) / 7)" + val request_2_2 = CalculationRequest(formula_2_2, variables) + val result_2_2 = calculator.calculate(request_2_2) + + assertTrue(result_2_2.isSuccess()) + // ์˜ˆ์ƒ๊ฐ’: 4 * (24/7) โ‰ˆ 13.714 + assertEquals(4.0 * 24.0 / 7.0, result_2_2.result as Double, 0.001) + + // 2ํ•™๋…„ 1ํ•™๊ธฐ ์ ์ˆ˜ (20์  ๋งŒ์ ) + val formula_2_1 = "4 * ((korean_2_1 + social_2_1 + history_2_1 + math_2_1 + science_2_1 + tech_2_1 + english_2_1) / 7)" + val request_2_1 = CalculationRequest(formula_2_1, variables) + val result_2_1 = calculator.calculate(request_2_1) + + assertTrue(result_2_1.isSuccess()) + // ์˜ˆ์ƒ๊ฐ’: 4 * (28/7) = 16.0 + assertEquals(16.0, result_2_1.result as Double, 0.001) + } + + @Test + @DisplayName("๊ต๊ณผ ๊ธฐ์ค€์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ (80์  ๋งŒ์ )") + fun `๊ต๊ณผ ๊ธฐ์ค€์ ์ˆ˜๊ฐ€ ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ํ™•์ธ`() { + val formula = """ + 8 * ((korean_3_1 + social_3_1 + history_3_1 + math_3_1 + science_3_1 + tech_3_1 + english_3_1) / 7) + + 4 * ((korean_2_2 + social_2_2 + history_2_2 + math_2_2 + science_2_2 + tech_2_2 + english_2_2) / 7) + + 4 * ((korean_2_1 + social_2_1 + history_2_1 + math_2_1 + science_2_1 + tech_2_1 + english_2_1) / 7) + """.trimIndent() + val request = CalculationRequest(formula, variables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + // ์˜ˆ์ƒ๊ฐ’: 30.857 + 13.714 + 16.0 โ‰ˆ 60.571 + val expected = 8.0 * 27.0 / 7.0 + 4.0 * 24.0 / 7.0 + 4.0 * 28.0 / 7.0 + assertEquals(expected, result.result as Double, 0.001) + } + + @Test + @DisplayName("์ผ๋ฐ˜์ „ํ˜• ๊ต๊ณผ์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ (140์  ๋งŒ์ )") + fun `์ผ๋ฐ˜์ „ํ˜• ๊ต๊ณผ์ ์ˆ˜๊ฐ€ ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ํ™•์ธ`() { + val formula = """ + (8 * ((korean_3_1 + social_3_1 + history_3_1 + math_3_1 + science_3_1 + tech_3_1 + english_3_1) / 7) + + 4 * ((korean_2_2 + social_2_2 + history_2_2 + math_2_2 + science_2_2 + tech_2_2 + english_2_2) / 7) + + 4 * ((korean_2_1 + social_2_1 + history_2_1 + math_2_1 + science_2_1 + tech_2_1 + english_2_1) / 7)) * 1.75 + """.trimIndent() + val request = CalculationRequest(formula, variables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + // ์˜ˆ์ƒ๊ฐ’: 60.571 * 1.75 โ‰ˆ 106.0 + val baseScore = 8.0 * 27.0 / 7.0 + 4.0 * 24.0 / 7.0 + 4.0 * 28.0 / 7.0 + val expected = baseScore * 1.75 + assertEquals(expected, result.result as Double, 0.001) + } + + @Test + @DisplayName("ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ") + fun `ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜๊ฐ€ ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ํ™•์ธ`() { + val formula = "absent_days + late_count/3 + early_leave_count/3 + lesson_absence_count/3" + val request = CalculationRequest(formula, variables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + // ์˜ˆ์ƒ๊ฐ’: 0 + 1/3 + 0/3 + 0/3 = 1/3 โ‰ˆ 0.333 + assertEquals(1.0/3.0, result.result as Double, 0.001) + } + + @Test + @DisplayName("๋ด‰์‚ฌํ™œ๋™์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ (15์  ๋งŒ์ )") + fun `๋ด‰์‚ฌํ™œ๋™์ ์ˆ˜๊ฐ€ ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ํ™•์ธ`() { + // MIN ํ•จ์ˆ˜๊ฐ€ ์—†๋‹ค๋ฉด IF ์กฐ๊ฑด๋ฌธ์œผ๋กœ ๋Œ€์ฒด + val formula = "IF(volunteer_hours > 15, 15, volunteer_hours)" + val request = CalculationRequest(formula, variables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + // 18์‹œ๊ฐ„ -> 15์  ๋งŒ์ ์ด๋ฏ€๋กœ 15์  + assertEquals(15.0, result.result as Double, 0.001) + } + + @Test + @DisplayName("๊ฐ€์‚ฐ์  ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ (3์  ๋งŒ์ )") + fun `๊ฐ€์‚ฐ์ ์ด ์ •ํ™•ํžˆ ๊ณ„์‚ฐ๋˜๋Š”์ง€ ํ™•์ธ`() { + val formula = "algorithm_award * 3 + info_license * 0" + val request = CalculationRequest(formula, variables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + // ์˜ˆ์ƒ๊ฐ’: 0 * 3 + 0 * 0 = 0 + assertEquals(0.0, result.result as Double, 0.001) + } + + @Test + @DisplayName("๋ณต์žกํ•œ IF ์กฐ๊ฑด๋ฌธ ์ถœ์„์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ (15์  ๋งŒ์ )") + fun `์ถœ์„์ ์ˆ˜ ๊ณ„์‚ฐ์ด ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ`() { + // ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜๊ฐ€ 1/3์ด๋ฏ€๋กœ ROUND(1/3 - 0.5) = ROUND(-0.167) = 0 + // 0์ผ์ด๋ฉด 15์  + val convertedAbsentDays = 0 // ROUND(1/3 - 0.5) = 0 + + // ๊ฐ„๋‹จํ•œ ํ…Œ์ŠคํŠธ: ๊ฒฐ์„์ผ์ˆ˜๊ฐ€ 0์ผ ๋•Œ 15์  + val formula = "IF(0 >= 1, 14, 15)" + val request = CalculationRequest(formula, variables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + assertEquals(15.0, result.result as Double, 0.001) + } + + @Test + @DisplayName("์ „์ฒด ์ ์ˆ˜ ๊ณ„์‚ฐ ํ†ตํ•ฉ ํ…Œ์ŠคํŠธ") + fun `์ „์ฒด ์ ์ˆ˜ ๊ณ„์‚ฐ์ด ์ •ํ™•ํžˆ ๋™์ž‘ํ•˜๋Š”์ง€ ํ™•์ธ`() { + // ๋‹จ๊ณ„๋ณ„ ๊ณ„์‚ฐ + val variables = this.variables.toMutableMap() + + // 1. ๊ต๊ณผ ์ ์ˆ˜ ๊ณ„์‚ฐ + val baseScore = 8.0 * 27.0 / 7.0 + 4.0 * 24.0 / 7.0 + 4.0 * 28.0 / 7.0 + val academicScore = baseScore * 1.75 + + // 2. ์ถœ์„ ์ ์ˆ˜ (ํ™˜์‚ฐ๊ฒฐ์„์ผ์ˆ˜ 0 -> 15์ ) + val attendanceScore = 15.0 + + // 3. ๋ด‰์‚ฌ ์ ์ˆ˜ (18์‹œ๊ฐ„ -> 15์ ) + val volunteerScore = 15.0 + + // 4. ๊ฐ€์‚ฐ์  (0์ ) + val bonusScore = 0.0 + + val expectedTotal = academicScore + attendanceScore + volunteerScore + bonusScore + + // ๊ฐ„๋‹จํ•œ ์ด์  ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ + val testVariables: MutableMap = variables.toMutableMap() + testVariables["academic_score"] = academicScore + testVariables["attendance_score"] = attendanceScore + testVariables["volunteer_score"] = volunteerScore + testVariables["bonus_score"] = bonusScore + + val formula = "academic_score + attendance_score + volunteer_score + bonus_score" + val request = CalculationRequest(formula, testVariables) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + assertEquals(expectedTotal, result.result as Double, 0.001) + + // ์˜ˆ์ƒ ์ด์  ์ถœ๋ ฅ + println("=== ๋Œ€๋•์†Œํ”„ํŠธ์›จ์–ด๋งˆ์ด์Šคํ„ฐ๊ณ ๋“ฑํ•™๊ต ํŠน๋ณ„์ „ํ˜• ์ ์ˆ˜ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ===") + println("๊ต๊ณผ์ ์ˆ˜: ${String.format("%.3f", academicScore)}์  (140์  ๋งŒ์ )") + println("์ถœ์„์ ์ˆ˜: ${attendanceScore.toInt()}์  (15์  ๋งŒ์ )") + println("๋ด‰์‚ฌ์ ์ˆ˜: ${volunteerScore.toInt()}์  (15์  ๋งŒ์ )") + println("๊ฐ€์‚ฐ์ : ${bonusScore.toInt()}์  (3์  ๋งŒ์ )") + println("์ด์ : ${String.format("%.3f", expectedTotal)}์  (173์  ๋งŒ์ )") + } + + @Test + @DisplayName("๊ทนํ•œ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ - ๋งŒ์  ํ•™์ƒ") + fun `๋งŒ์  ํ•™์ƒ์˜ ์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ`() { + val perfectVariables = mapOf( + // ๋ชจ๋“  ๊ณผ๋ชฉ 5์  (๋งŒ์ ) + "korean_3_1" to 5, "social_3_1" to 5, "history_3_1" to 5, "math_3_1" to 5, + "science_3_1" to 5, "tech_3_1" to 5, "english_3_1" to 5, + "korean_2_2" to 5, "social_2_2" to 5, "history_2_2" to 5, "math_2_2" to 5, + "science_2_2" to 5, "tech_2_2" to 5, "english_2_2" to 5, + "korean_2_1" to 5, "social_2_1" to 5, "history_2_1" to 5, "math_2_1" to 5, + "science_2_1" to 5, "tech_2_1" to 5, "english_2_1" to 5, + + // ์™„๋ฒฝํ•œ ์ถœ๊ฒฐ + "absent_days" to 0, "late_count" to 0, "early_leave_count" to 0, "lesson_absence_count" to 0, + + // ์ตœ๋Œ€ ๋ด‰์‚ฌ์‹œ๊ฐ„๊ณผ ๊ฐ€์‚ฐ์  + "volunteer_hours" to 20, "algorithm_award" to 1, "info_license" to 1 + ) + + // ๊ต๊ณผ์ ์ˆ˜: 80 * 1.75 = 140์  + val academicScore = 140.0 + val attendanceScore = 15.0 + val volunteerScore = 15.0 + val bonusScore = 3.0 // 1 * 3 + 1 * 0 = 3 + val expectedTotal = 173.0 // ๋งŒ์  + + val perfectVars: MutableMap = perfectVariables.toMutableMap() + perfectVars["academic_score"] = academicScore + perfectVars["attendance_score"] = attendanceScore + perfectVars["volunteer_score"] = volunteerScore + perfectVars["bonus_score"] = bonusScore + + val formula = "academic_score + attendance_score + volunteer_score + bonus_score" + val request = CalculationRequest(formula, perfectVars) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + assertEquals(expectedTotal, result.result as Double, 0.001) + + println("=== ๋งŒ์  ํ•™์ƒ ์ ์ˆ˜ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ===") + println("๊ต๊ณผ์ ์ˆ˜: ${academicScore.toInt()}์  (140์  ๋งŒ์ )") + println("์ถœ์„์ ์ˆ˜: ${attendanceScore.toInt()}์  (15์  ๋งŒ์ )") + println("๋ด‰์‚ฌ์ ์ˆ˜: ${volunteerScore.toInt()}์  (15์  ๋งŒ์ )") + println("๊ฐ€์‚ฐ์ : ${bonusScore.toInt()}์  (3์  ๋งŒ์ )") + println("์ด์ : ${expectedTotal.toInt()}์  (173์  ๋งŒ์ ) - ๋งŒ์ !") + } + + @Test + @DisplayName("๊ทนํ•œ ์ผ€์ด์Šค ํ…Œ์ŠคํŠธ - ์ตœ์ €์  ํ•™์ƒ") + fun `์ตœ์ €์  ํ•™์ƒ์˜ ์ ์ˆ˜ ๊ณ„์‚ฐ ํ…Œ์ŠคํŠธ`() { + val minVariables = mapOf( + // ๋ชจ๋“  ๊ณผ๋ชฉ 1์  (์ตœ์ €์ ) + "korean_3_1" to 1, "social_3_1" to 1, "history_3_1" to 1, "math_3_1" to 1, + "science_3_1" to 1, "tech_3_1" to 1, "english_3_1" to 1, + "korean_2_2" to 1, "social_2_2" to 1, "history_2_2" to 1, "math_2_2" to 1, + "science_2_2" to 1, "tech_2_2" to 1, "english_2_2" to 1, + "korean_2_1" to 1, "social_2_1" to 1, "history_2_1" to 1, "math_2_1" to 1, + "science_2_1" to 1, "tech_2_1" to 1, "english_2_1" to 1, + + // ์ตœ์•…์˜ ์ถœ๊ฒฐ (15์ผ ์ด์ƒ ๊ฒฐ์„ -> 0์ ) + "absent_days" to 20, "late_count" to 0, "early_leave_count" to 0, "lesson_absence_count" to 0, + + // ๋ด‰์‚ฌ์‹œ๊ฐ„ ์—†์Œ, ๊ฐ€์‚ฐ์  ์—†์Œ + "volunteer_hours" to 0, "algorithm_award" to 0, "info_license" to 0 + ) + + // ๊ต๊ณผ์ ์ˆ˜: 16 * 1.75 = 28์  (๋ชจ๋“  ๊ณผ๋ชฉ 1์ ์ผ ๋•Œ) + val academicScore = 16.0 * 1.75 // 28์  + val attendanceScore = 0.0 // 15์ผ ์ด์ƒ ๊ฒฐ์„ + val volunteerScore = 0.0 + val bonusScore = 0.0 + val expectedTotal = academicScore // 28์  + + val minVars: MutableMap = minVariables.toMutableMap() + minVars["academic_score"] = academicScore + minVars["attendance_score"] = attendanceScore + minVars["volunteer_score"] = volunteerScore + minVars["bonus_score"] = bonusScore + + val formula = "academic_score + attendance_score + volunteer_score + bonus_score" + val request = CalculationRequest(formula, minVars) + + val result = calculator.calculate(request) + + assertTrue(result.isSuccess()) + assertEquals(expectedTotal, result.result as Double, 0.001) + + println("=== ์ตœ์ €์  ํ•™์ƒ ์ ์ˆ˜ ๊ณ„์‚ฐ ๊ฒฐ๊ณผ ===") + println("๊ต๊ณผ์ ์ˆ˜: ${String.format("%.1f", academicScore)}์  (140์  ๋งŒ์ )") + println("์ถœ์„์ ์ˆ˜: ${attendanceScore.toInt()}์  (15์  ๋งŒ์ )") + println("๋ด‰์‚ฌ์ ์ˆ˜: ${volunteerScore.toInt()}์  (15์  ๋งŒ์ )") + println("๊ฐ€์‚ฐ์ : ${bonusScore.toInt()}์  (3์  ๋งŒ์ )") + println("์ด์ : ${String.format("%.1f", expectedTotal)}์  (173์  ๋งŒ์ )") + } +} \ No newline at end of file diff --git a/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/factories/CalculatorFactoryTest.kt b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/factories/CalculatorFactoryTest.kt new file mode 100644 index 00000000..94815f4d --- /dev/null +++ b/casper-application-domain/src/test/kotlin/hs/kr/entrydsm/domain/calculator/factories/CalculatorFactoryTest.kt @@ -0,0 +1,477 @@ +package hs.kr.entrydsm.domain.calculator.factories + +import hs.kr.entrydsm.domain.calculator.aggregates.Calculator +import hs.kr.entrydsm.domain.calculator.entities.CalculationSession +import hs.kr.entrydsm.domain.calculator.exceptions.CalculatorException +import hs.kr.entrydsm.domain.calculator.values.CalculationRequest +import hs.kr.entrydsm.domain.calculator.values.CalculationResult +import hs.kr.entrydsm.global.annotation.specification.type.Priority +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import kotlin.test.assertNotNull +import kotlin.test.assertFalse +import kotlin.test.assertContains + +/** + * CalculatorFactory์˜ ํŽธ์˜ ๋ฉ”์„œ๋“œ๋“ค์„ ํ…Œ์ŠคํŠธํ•˜๋Š” ํด๋ž˜์Šค์ž…๋‹ˆ๋‹ค. + */ +class CalculatorFactoryTest { + + private lateinit var factory: CalculatorFactory + + @BeforeEach + fun setUp() { + factory = CalculatorFactory() + } + + @Test + fun `createBasicCalculator๊ฐ€ ๊ธฐ๋ณธ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val calculator = factory.createBasicCalculator() + + assertNotNull(calculator) + assertTrue(calculator is Calculator) + } + + @Test + fun `createScientificCalculator๊ฐ€ ๊ณผํ•™ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val calculator = factory.createScientificCalculator() + + assertNotNull(calculator) + assertTrue(calculator is Calculator) + } + + @Test + fun `createStatisticalCalculator๊ฐ€ ํ†ต๊ณ„ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val calculator = factory.createStatisticalCalculator() + + assertNotNull(calculator) + assertTrue(calculator is Calculator) + } + + @Test + fun `createEngineeringCalculator๊ฐ€ ๊ณตํ•™์šฉ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val calculator = factory.createEngineeringCalculator() + + assertNotNull(calculator) + assertTrue(calculator is Calculator) + } + + @Test + fun `createSession์ด ๊ณ„์‚ฐ ์„ธ์…˜์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val session = factory.createSession("testUser") + + assertNotNull(session) + assertTrue(session is CalculationSession) + } + + @Test + fun `createSession์ด ์ต๋ช… ์„ธ์…˜์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val session = factory.createSession() + + assertNotNull(session) + assertTrue(session is CalculationSession) + } + + @Test + fun `createRequest๊ฐ€ ๊ณ„์‚ฐ ์š”์ฒญ์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val request = factory.createRequest("2 + 3") + + assertNotNull(request) + assertTrue(request is CalculationRequest) + assertEquals("2 + 3", request.formula) + } + + @Test + fun `createRequest๊ฐ€ ๋ณ€์ˆ˜๊ฐ€ ํฌํ•จ๋œ ๊ณ„์‚ฐ ์š”์ฒญ์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val variables = mapOf("x" to 10.0, "y" to 5.0) + val request = factory.createRequest("x + y", variables) + + assertNotNull(request) + assertTrue(request is CalculationRequest) + assertEquals("x + y", request.formula) + assertEquals(variables, request.variables) + } + + @Test + fun `Companion object์˜ getInstance๊ฐ€ ์‹ฑ๊ธ€ํ†ค์„ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val instance1 = CalculatorFactory.getInstance() + val instance2 = CalculatorFactory.getInstance() + + assertNotNull(instance1) + assertNotNull(instance2) + assertEquals(instance1, instance2) // ๊ฐ™์€ ์ธ์Šคํ„ด์Šค์—ฌ์•ผ ํ•จ + } + + @Test + fun `Companion object์˜ ํŽธ์˜ ๋ฉ”์„œ๋“œ๋“ค์ด ๋™์ž‘ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val basicCalculator = CalculatorFactory.quickCreateBasicCalculator() + assertNotNull(basicCalculator) + assertTrue(basicCalculator is Calculator) + + val scientificCalculator = CalculatorFactory.quickCreateScientificCalculator() + assertNotNull(scientificCalculator) + assertTrue(scientificCalculator is Calculator) + + val session = CalculatorFactory.quickCreateSession("quickUser") + assertNotNull(session) + assertTrue(session is CalculationSession) + + val request = CalculatorFactory.quickCreateRequest("1 + 1") + assertNotNull(request) + assertTrue(request is CalculationRequest) + assertEquals("1 + 1", request.formula) + } + + @Test + fun `์—ฌ๋Ÿฌ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val basicCalc1 = factory.createBasicCalculator() + val basicCalc2 = factory.createBasicCalculator() + val scientificCalc = factory.createScientificCalculator() + val engineeringCalc = factory.createEngineeringCalculator() + val statisticalCalc = factory.createStatisticalCalculator() + + // ๋ชจ๋“  ๊ณ„์‚ฐ๊ธฐ๊ฐ€ ์„ฑ๊ณต์ ์œผ๋กœ ์ƒ์„ฑ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ + assertNotNull(basicCalc1) + assertNotNull(basicCalc2) + assertNotNull(scientificCalc) + assertNotNull(engineeringCalc) + assertNotNull(statisticalCalc) + + // ๊ฐ๊ฐ ๋‹ค๋ฅธ ์ธ์Šคํ„ด์Šค์ธ์ง€ ํ™•์ธ (Calculator์˜ equals ๊ตฌํ˜„์— ๋”ฐ๋ผ) + assertTrue(basicCalc1 is Calculator) + assertTrue(basicCalc2 is Calculator) + assertTrue(scientificCalc is Calculator) + assertTrue(engineeringCalc is Calculator) + assertTrue(statisticalCalc is Calculator) + } + + @Test + fun `์—ฌ๋Ÿฌ ์„ธ์…˜์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val session1 = factory.createSession("user1") + val session2 = factory.createSession("user2") + val anonymousSession = factory.createSession() + + assertNotNull(session1) + assertNotNull(session2) + assertNotNull(anonymousSession) + + assertTrue(session1 is CalculationSession) + assertTrue(session2 is CalculationSession) + assertTrue(anonymousSession is CalculationSession) + } + + @Test + fun `๋‹ค์–‘ํ•œ ํ‘œํ˜„์‹์œผ๋กœ ์š”์ฒญ์„ ์ƒ์„ฑํ•  ์ˆ˜ ์žˆ๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val simpleRequest = factory.createRequest("2 + 2") + val complexRequest = factory.createRequest("sin(x) * cos(y) + sqrt(z)", mapOf("x" to 1.0, "y" to 2.0, "z" to 4.0)) + val functionRequest = factory.createRequest("max(a, b, c)", mapOf("a" to 10, "b" to 20, "c" to 15)) + + assertNotNull(simpleRequest) + assertNotNull(complexRequest) + assertNotNull(functionRequest) + + assertEquals("2 + 2", simpleRequest.formula) + assertEquals("sin(x) * cos(y) + sqrt(z)", complexRequest.formula) + assertEquals("max(a, b, c)", functionRequest.formula) + + assertTrue(simpleRequest.variables.isEmpty()) + assertEquals(3, complexRequest.variables.size) + assertEquals(3, functionRequest.variables.size) + } + + @Test + fun `ํŒฉํ† ๋ฆฌ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ธ์Šคํ„ด์Šคํ™”๋˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + assertNotNull(factory) + } + + @Test + fun `createCustomCalculator๊ฐ€ ์‚ฌ์šฉ์ž ์ •์˜ ์„ค์ •์œผ๋กœ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val customCalculator = factory.createCustomCalculator( + precision = 15, + enableCaching = true, + enableOptimization = false + ) + + assertNotNull(customCalculator) + assertTrue(customCalculator is Calculator) + } + + @Test + fun `createUserSession์ด ์‚ฌ์šฉ์ž ์„ธ์…˜์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val userSession = factory.createUserSession("specificUser") + + assertNotNull(userSession) + assertTrue(userSession is CalculationSession) + } + + @Test + fun `createTemporarySession์ด ์ž„์‹œ ์„ธ์…˜์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val tempSession = factory.createTemporarySession() + + assertNotNull(tempSession) + assertTrue(tempSession is CalculationSession) + } + + @Test + fun `createCustomSession์ด ์‚ฌ์šฉ์ž ์ •์˜ ์„ธ์…˜์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val sessionId = "custom-session-123" + val userId = "custom-user" + val variables = mapOf("pi" to 3.14159, "e" to 2.71828) + + val customSession = factory.createCustomSession( + sessionId = sessionId, + userId = userId, + variables = variables + ) + + assertNotNull(customSession) + assertTrue(customSession is CalculationSession) + } + + @Test + fun `createPriorityRequest๊ฐ€ ์šฐ์„ ์ˆœ์œ„ ์š”์ฒญ์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val priorityRequest = factory.createPriorityRequest( + "2 * 3 + 4", + Priority.HIGH + ) + + assertNotNull(priorityRequest) + assertTrue(priorityRequest is CalculationRequest) + assertEquals("2 * 3 + 4", priorityRequest.formula) + assertTrue(priorityRequest.hasOption("priority")) + assertEquals("HIGH", priorityRequest.getOption("priority")) + } + + @Test + fun `createBatchRequests๊ฐ€ ์ผ๊ด„ ์š”์ฒญ๋“ค์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val expressions = listOf("1 + 1", "2 * 3", "10 / 2", "5 - 3") + val variables = mapOf("x" to 5.0) + + val batchRequests = factory.createBatchRequests(expressions, variables) + + assertNotNull(batchRequests) + assertEquals(4, batchRequests.size) + + batchRequests.forEachIndexed { index, request -> + assertEquals(expressions[index], request.formula) + assertEquals(variables, request.variables) + } + } + + @Test + fun `createSuccessResult๊ฐ€ ์„ฑ๊ณต ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val successResult = factory.createSuccessResult( + formula = "2 + 3", + result = 5.0, + executionTimeMs = 100L + ) + + assertNotNull(successResult) + assertTrue(successResult is CalculationResult) + assertEquals(5.0, successResult.result) + assertEquals(100L, successResult.executionTimeMs) + assertEquals("2 + 3", successResult.formula) + assertTrue(successResult.isSuccess()) + } + + @Test + fun `createFailureResult๊ฐ€ ์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val failureResult = factory.createFailureResult( + formula = "1 / 0", + error = "Division by zero", + executionTimeMs = 50L + ) + + assertNotNull(failureResult) + assertTrue(failureResult is CalculationResult) + assertEquals(null, failureResult.result) + assertEquals(50L, failureResult.executionTimeMs) + assertEquals("1 / 0", failureResult.formula) + assertFalse(failureResult.isSuccess()) + assertTrue(failureResult.errors.contains("Division by zero")) + } + + @Test + fun `createFailureFromException์ด ์˜ˆ์™ธ๋กœ๋ถ€ํ„ฐ ์‹คํŒจ ๊ฒฐ๊ณผ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val exception = IllegalArgumentException("Invalid argument") + val failureResult = factory.createFailureFromException( + formula = "invalid_function(x)", + exception = exception, + executionTimeMs = 25L + ) + + assertNotNull(failureResult) + assertTrue(failureResult is CalculationResult) + assertEquals(null, failureResult.result) + assertEquals(25L, failureResult.executionTimeMs) + assertEquals("invalid_function(x)", failureResult.formula) + assertFalse(failureResult.isSuccess()) + assertContains(failureResult.errors.first(), "Invalid argument") + } + + @Test + fun `createDefaultEnvironment๊ฐ€ ๊ธฐ๋ณธ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val defaultEnv = factory.createDefaultEnvironment() + + assertNotNull(defaultEnv) + assertTrue(defaultEnv.containsKey("PI")) + assertTrue(defaultEnv.containsKey("E")) + assertTrue(defaultEnv.containsKey("TRUE")) + assertTrue(defaultEnv.containsKey("FALSE")) + assertEquals(kotlin.math.PI, defaultEnv["PI"]) + assertEquals(kotlin.math.E, defaultEnv["E"]) + } + + @Test + fun `createScientificEnvironment๊ฐ€ ๊ณผํ•™ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val scientificEnv = factory.createScientificEnvironment() + + assertNotNull(scientificEnv) + assertTrue(scientificEnv.containsKey("PI")) + assertTrue(scientificEnv.containsKey("E")) + assertTrue(scientificEnv.containsKey("LIGHT_SPEED")) + assertTrue(scientificEnv.containsKey("PLANCK")) + assertTrue(scientificEnv.containsKey("AVOGADRO")) + assertEquals(299792458.0, scientificEnv["LIGHT_SPEED"]) + } + + @Test + fun `createEngineeringEnvironment๊ฐ€ ๊ณตํ•™ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val engineeringEnv = factory.createEngineeringEnvironment() + + assertNotNull(engineeringEnv) + assertTrue(engineeringEnv.containsKey("GRAVITY")) + assertTrue(engineeringEnv.containsKey("ATMOSPHERIC_PRESSURE")) + assertTrue(engineeringEnv.containsKey("ABSOLUTE_ZERO")) + assertEquals(9.80665, engineeringEnv["GRAVITY"]) + assertEquals(101325.0, engineeringEnv["ATMOSPHERIC_PRESSURE"]) + } + + @Test + fun `createStatisticalEnvironment๊ฐ€ ํ†ต๊ณ„ ํ™˜๊ฒฝ์„ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val statisticalEnv = factory.createStatisticalEnvironment() + + assertNotNull(statisticalEnv) + assertTrue(statisticalEnv.containsKey("SQRT_2PI")) + assertTrue(statisticalEnv.containsKey("LN_2")) + assertTrue(statisticalEnv.containsKey("LN_10")) + assertEquals(kotlin.math.sqrt(2 * kotlin.math.PI), statisticalEnv["SQRT_2PI"]) + } + + @Test + fun `createHighPerformanceCalculator๊ฐ€ ๊ณ ์„ฑ๋Šฅ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val highPerfCalculator = factory.createHighPerformanceCalculator( + maxConcurrency = 20, + cacheSize = 2000 + ) + + assertNotNull(highPerfCalculator) + assertTrue(highPerfCalculator is Calculator) + } + + @Test + fun `createSecureCalculator๊ฐ€ ๋ณด์•ˆ ๊ณ„์‚ฐ๊ธฐ๋ฅผ ์ƒ์„ฑํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val secureCalculator = factory.createSecureCalculator() + + assertNotNull(secureCalculator) + assertTrue(secureCalculator is Calculator) + } + + @Test + fun `getStatistics๊ฐ€ ํŒฉํ† ๋ฆฌ ํ†ต๊ณ„๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + // ๋ช‡ ๊ฐœ์˜ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ํ†ต๊ณ„๋ฅผ ๋ˆ„์  + factory.createBasicCalculator() + factory.createSession("user1") + factory.createRequest("1 + 1") + + val statistics = factory.getStatistics() + + assertNotNull(statistics) + assertTrue(statistics.containsKey("factoryName")) + assertTrue(statistics.containsKey("createdCalculators")) + assertTrue(statistics.containsKey("createdSessions")) + assertTrue(statistics.containsKey("createdRequests")) + assertEquals("CalculatorFactory", statistics["factoryName"]) + } + + @Test + fun `getConfiguration์ด ํŒฉํ† ๋ฆฌ ์„ค์ •์„ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val configuration = factory.getConfiguration() + + assertNotNull(configuration) + assertTrue(configuration.containsKey("defaultPrecision")) + assertTrue(configuration.containsKey("defaultAngleUnit")) + assertTrue(configuration.containsKey("defaultCachingEnabled")) + assertEquals(10, configuration["defaultPrecision"]) + assertEquals("RADIANS", configuration["defaultAngleUnit"]) + assertEquals(true, configuration["defaultCachingEnabled"]) + } + + @Test + fun `createUserSession์ด ๋นˆ ์‚ฌ์šฉ์ž ID๋กœ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + assertThrows { + factory.createUserSession("") + } + + assertThrows { + factory.createUserSession(" ") + } + } + + @Test + fun `createRequest๊ฐ€ ๋นˆ ์ˆ˜์‹์œผ๋กœ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + assertThrows { + factory.createRequest("") + } + + assertThrows { + factory.createRequest(" ") + } + } + + @Test + fun `createBatchRequests๊ฐ€ ๋นˆ ํ‘œํ˜„์‹ ๋ชฉ๋ก์œผ๋กœ ์˜ˆ์™ธ๋ฅผ ๋ฐœ์ƒ์‹œํ‚ค๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + assertThrows { + factory.createBatchRequests(emptyList()) + } + } + + @Test + fun `ํŒฉํ† ๋ฆฌ๊ฐ€ DDD Factory ํŒจํ„ด์„ ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ๊ตฌํ˜„ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + // Factory ํŒจํ„ด ๊ฒ€์ฆ: ์ผ๊ด€๋œ ๊ฐ์ฒด ์ƒ์„ฑ + val calculator1 = factory.createBasicCalculator() + val calculator2 = factory.createBasicCalculator() + + // ๊ฐ๊ฐ ๋…๋ฆฝ์ ์ธ ์ธ์Šคํ„ด์Šค์—ฌ์•ผ ํ•จ + assertTrue(calculator1 !== calculator2) + + // ํ•˜์ง€๋งŒ ๊ฐ™์€ ํƒ€์ž…์ด์–ด์•ผ ํ•จ + assertEquals(calculator1::class, calculator2::class) + } + + @Test + fun `ํŒฉํ† ๋ฆฌ ํ†ต๊ณ„๊ฐ€ ๊ฐ์ฒด ์ƒ์„ฑ์„ ์ •ํ™•ํžˆ ์ถ”์ ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธ`() { + val initialStats = factory.getStatistics() + val initialCalculators = initialStats["createdCalculators"] as Long + val initialSessions = initialStats["createdSessions"] as Long + val initialRequests = initialStats["createdRequests"] as Long + + // ์ƒˆ ๊ฐ์ฒด๋“ค ์ƒ์„ฑ + factory.createBasicCalculator() + factory.createScientificCalculator() + factory.createSession("test") + factory.createRequest("test") + + val updatedStats = factory.getStatistics() + val updatedCalculators = updatedStats["createdCalculators"] as Long + val updatedSessions = updatedStats["createdSessions"] as Long + val updatedRequests = updatedStats["createdRequests"] as Long + + assertEquals(initialCalculators + 2, updatedCalculators) + assertEquals(initialSessions + 1, updatedSessions) + assertEquals(initialRequests + 1, updatedRequests) + } +} \ No newline at end of file