Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
88 changes: 82 additions & 6 deletions src/main/kotlin/dev/jetpack/engine/parser/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -237,16 +237,20 @@ class Parser(private val tokens: List<Token>) {
TokenType.KW_COMMAND -> parseCommandDecl(access ?: AccessModifier.PRIVATE, line, isRoot = true)
TokenType.KW_CONST -> {
advance()
if (!isTypeKeyword(peekType())) {
if (peekType() == TokenType.LBRACE || peekType() == TokenType.LBRACKET) {
parseDestructuring(access ?: AccessModifier.PRIVATE, isConst = true, line)
} else if (!isTypeKeyword(peekType())) {
throw ParseException("Const can only be used with variable declarations", line)
} else {
parseVarDecl(access ?: AccessModifier.PRIVATE, isConst = true, line)
}
parseVarDecl(access ?: AccessModifier.PRIVATE, isConst = true, line)
}
else -> {
if (access != null && !isTypeKeyword(peekType())) {
if ((peekType() == TokenType.LBRACE || peekType() == TokenType.LBRACKET) && isDestructuringAhead()) {
parseDestructuring(access ?: AccessModifier.PRIVATE, isConst = false, line)
} else if (access != null && !isTypeKeyword(peekType())) {
throw ParseException("Access modifier can only be used with top-level declarations", line)
}
if (isTypeKeyword(peekType())) {
} else if (isTypeKeyword(peekType())) {
parseVarDecl(access ?: AccessModifier.PRIVATE, isConst = false, line)
} else {
if (access != null) {
Expand Down Expand Up @@ -697,7 +701,7 @@ class Parser(private val tokens: List<Token>) {

private fun parseComparison(): Expression {
var left = parseRange()
while (peekType() in COMPARISON_OPS) {
while (peekType() in COMPARISON_OPS || peekType() == TokenType.KW_IN) {
val op = advance()
left = Expression.BinaryOp(left, op, parseRange(), op.line)
}
Expand Down Expand Up @@ -1063,4 +1067,76 @@ class Parser(private val tokens: List<Token>) {
expect(TokenType.RBRACE, "Expected '}' to close object literal")
return Expression.ObjectLiteral(entries, line)
}

private fun isDestructuringAhead(): Boolean {
val openType = peekType()
if (openType != TokenType.LBRACE && openType != TokenType.LBRACKET) return false
val closeType = if (openType == TokenType.LBRACE) TokenType.RBRACE else TokenType.RBRACKET
var i = pos + 1
var depth = 0
while (i < tokens.size) {
when (tokens[i].type) {
openType -> depth++
closeType -> {
if (depth == 0) {
i++
while (i < tokens.size && (tokens[i].type == TokenType.NEWLINE || tokens[i].type == TokenType.SEMICOLON)) i++
return i < tokens.size && tokens[i].type == TokenType.EQ
}
depth--
}
TokenType.EOF -> return false
else -> {}
}
i++
}
return false
}

private fun parseDestructuring(access: AccessModifier, isConst: Boolean, line: Int): Statement =
when (peekType()) {
TokenType.LBRACE -> parseObjectDestructuring(access, isConst, line)
TokenType.LBRACKET -> parseListDestructuring(access, isConst, line)
else -> throw ParseException("Expected '{' or '[' for destructuring", line)
}

private fun parseObjectDestructuring(access: AccessModifier, isConst: Boolean, line: Int): Statement.ObjectDestructuring {
expect(TokenType.LBRACE, "Expected '{'")
skipNewlines()
val bindings = mutableListOf<DestructuringBinding>()
while (!check(TokenType.RBRACE) && !isAtEnd()) {
val fieldName = expect(TokenType.IDENTIFIER, "Expected field name in object destructuring").value
val localName = if (check(TokenType.COLON)) {
advance()
expect(TokenType.IDENTIFIER, "Expected local variable name after ':' in object destructuring").value
} else {
fieldName
}
bindings.add(DestructuringBinding(fieldName, localName))
skipNewlines()
if (check(TokenType.COMMA)) { advance(); skipNewlines() }
}
expect(TokenType.RBRACE, "Expected '}' to close object destructuring")
expect(TokenType.EQ, "Expected '=' after object destructuring pattern")
val initializer = parseExpression()
return Statement.ObjectDestructuring(access, isConst, bindings, initializer, line)
}

private fun parseListDestructuring(access: AccessModifier, isConst: Boolean, line: Int): Statement.ListDestructuring {
expect(TokenType.LBRACKET, "Expected '['")
skipNewlines()
val bindings = mutableListOf<String?>()
while (!check(TokenType.RBRACKET) && !isAtEnd()) {
bindings.add(
if (check(TokenType.IDENTIFIER) && peek().value == "_") { advance(); null }
else expect(TokenType.IDENTIFIER, "Expected variable name or '_' in list destructuring").value
)
skipNewlines()
if (check(TokenType.COMMA)) { advance(); skipNewlines() }
}
expect(TokenType.RBRACKET, "Expected ']' to close list destructuring")
expect(TokenType.EQ, "Expected '=' after list destructuring pattern")
val initializer = parseExpression()
return Statement.ListDestructuring(access, isConst, bindings, initializer, line)
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/dev/jetpack/engine/parser/ast/Statement.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,20 @@ sealed class Statement {
val annotations: CommandAnnotations,
override val line: Int,
) : Statement()
data class ObjectDestructuring(
val access: AccessModifier,
val isConst: Boolean,
val bindings: List<DestructuringBinding>,
val initializer: Expression,
override val line: Int,
) : Statement()
data class ListDestructuring(
val access: AccessModifier,
val isConst: Boolean,
val bindings: List<String?>,
val initializer: Expression,
override val line: Int,
) : Statement()
}

data class CommandAnnotations(
Expand Down Expand Up @@ -122,4 +136,9 @@ data class TypeRef(
val typeArgRef: TypeRef? = null,
)

data class DestructuringBinding(
val fieldName: String,
val localName: String,
)

enum class AccessModifier { PUBLIC, PRIVATE, PROTECTED }
10 changes: 10 additions & 0 deletions src/main/kotlin/dev/jetpack/engine/resolver/NameResolver.kt
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,16 @@ class NameResolver(private val reservedNames: Set<String> = emptySet()) {
insideLoop = prevLoop
isFileScope = prevFile
}

is Statement.ObjectDestructuring -> {
resolveExpr(stmt.initializer)
for (binding in stmt.bindings) declare(binding.localName, stmt.line)
}

is Statement.ListDestructuring -> {
resolveExpr(stmt.initializer)
for (name in stmt.bindings) if (name != null) declare(name, stmt.line)
}
}
}

Expand Down
47 changes: 46 additions & 1 deletion src/main/kotlin/dev/jetpack/engine/resolver/TypeChecker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,27 @@ class TypeChecker(private val typeProvider: BuiltinTypeProvider? = null) {

is Statement.BreakStmt, is Statement.ContinueStmt -> Unit

is Statement.ObjectDestructuring -> {
val initType = inferExpr(stmt.initializer)
if (initType != JetType.TObject && initType != JetType.TUnknown) {
errors.add(TypeCheckerError("Object destructuring requires an object, got '$initType'", stmt.line))
}
for (binding in stmt.bindings) {
defineType(binding.localName, JetType.TUnknown, stmt.line, stmt.isConst)
}
}

is Statement.ListDestructuring -> {
val initType = inferExpr(stmt.initializer)
if (initType !is JetType.TList && initType != JetType.TUnknown) {
errors.add(TypeCheckerError("List destructuring requires a list, got '$initType'", stmt.line))
}
val elementType = if (initType is JetType.TList) initType.elementType else JetType.TUnknown
for (name in stmt.bindings) {
if (name != null) defineType(name, elementType, stmt.line, stmt.isConst)
}
}

is Statement.CommandDecl -> checkCommandDecl(stmt, stmt.senderName)
}
}
Expand Down Expand Up @@ -675,6 +696,28 @@ class TypeChecker(private val typeProvider: BuiltinTypeProvider? = null) {
left == JetType.TInt && right == JetType.TInt -> JetType.TInt
else -> JetType.TFloat
}
op == TokenType.KW_IN -> {
if (isNullableType(left)) {
errors.add(TypeCheckerError("Operator 'in' cannot be applied to nullable type '$left'", expr.line))
}
when {
right is JetType.TList -> Unit
right == JetType.TObject -> {
if (left != JetType.TString && left != JetType.TUnknown) {
errors.add(TypeCheckerError("'in' on object requires a string operand, got '$left'", expr.line))
}
}
right == JetType.TString -> {
if (left != JetType.TString && left != JetType.TUnknown) {
errors.add(TypeCheckerError("'in' on string requires a string operand, got '$left'", expr.line))
}
}
right != JetType.TUnknown -> {
errors.add(TypeCheckerError("Operator 'in' cannot be applied to type '$right'", expr.line))
}
}
JetType.TBool
}
isNullableType(left) || isNullableType(right) -> {
val nullableType = if (isNullableType(left)) left else right
errors.add(TypeCheckerError(
Expand Down Expand Up @@ -1088,7 +1131,9 @@ class TypeChecker(private val typeProvider: BuiltinTypeProvider? = null) {
is Statement.FunctionDecl,
is Statement.IntervalDecl,
is Statement.ListenerDecl,
is Statement.CommandDecl -> setOf(FlowSignal.FALLTHROUGH)
is Statement.CommandDecl,
is Statement.ObjectDestructuring,
is Statement.ListDestructuring -> setOf(FlowSignal.FALLTHROUGH)
}

private fun analyzeTryFlow(stmt: Statement.TryStmt): Set<FlowSignal> {
Expand Down
61 changes: 61 additions & 0 deletions src/main/kotlin/dev/jetpack/engine/runtime/Interpreter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,41 @@ class Interpreter(
}
is Statement.BreakStmt -> throw BreakSignal()
is Statement.ContinueStmt -> throw ContinueSignal()
is Statement.ObjectDestructuring -> {
val obj = evalExpr(stmt.initializer, scope)
if (obj !is JObject) throw RuntimeError(
"Object destructuring requires an object, got '${obj.typeName()}'",
stmt.line, "TypeException",
)
for (binding in stmt.bindings) {
val value = obj.getField(binding.fieldName)
?: throw RuntimeError(
"Object field '${binding.fieldName}' does not exist",
stmt.line, "KeyException",
)
withScopeRuntimeError(stmt.line) {
scope.defineCoerced(binding.localName, value, stmt.isConst, null)
}
}
}
is Statement.ListDestructuring -> {
val list = evalExpr(stmt.initializer, scope)
if (list !is JList) throw RuntimeError(
"List destructuring requires a list, got '${list.typeName()}'",
stmt.line, "TypeException",
)
for ((index, name) in stmt.bindings.withIndex()) {
if (name == null) continue
val value = list.elements.getOrNull(index)
?: throw RuntimeError(
"List destructuring index $index is out of range (list has ${list.elements.size} elements)",
stmt.line, "IndexException",
)
withScopeRuntimeError(stmt.line) {
scope.defineCoerced(name, value, stmt.isConst, null)
}
}
}
is Statement.Metadata, is Statement.Using, is Statement.Manifest, is Statement.CommandDecl -> Unit
}
}
Expand Down Expand Up @@ -778,6 +813,32 @@ class Interpreter(
JString(right.value.repeat(count))
}
left.isNumeric() && right.isNumeric() -> evalNumericOp(left, right, op, expr.line)
op == TokenType.KW_IN -> when (right) {
is JList -> JBool(right.elements.any { jetEquals(left, it) })
is JObject -> {
val key = (left as? JString)
?: throw RuntimeError(
"'in' on object requires a string key, got '${left.typeName()}'",
expr.line,
"TypeException",
)
JBool(right.hasField(key.value))
}
is JString -> {
val sub = (left as? JString)
?: throw RuntimeError(
"'in' on string requires a string value, got '${left.typeName()}'",
expr.line,
"TypeException",
)
JBool(right.value.contains(sub.value))
}
else -> throw RuntimeError(
"Operator 'in' cannot be applied to type '${right.typeName()}'",
expr.line,
"TypeException",
)
}
else -> throw RuntimeError(
"Operator '${expr.operator.value}' cannot be applied to types '${left.typeName()}' and '${right.typeName()}'",
expr.line,
Expand Down
24 changes: 24 additions & 0 deletions src/main/kotlin/dev/jetpack/script/ScriptModuleFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,30 @@ internal class ScriptModuleFactory(private val scriptsRoot: File) {
availableAfterDeclaration = true,
)
}
is Statement.ObjectDestructuring -> {
for (binding in stmt.bindings) {
exports[binding.localName] = ModuleExportDefinition(
name = binding.localName,
access = stmt.access,
type = JetType.TUnknown,
isReadOnly = stmt.isConst || stmt.access == AccessModifier.PROTECTED,
availableAfterDeclaration = false,
)
}
}
is Statement.ListDestructuring -> {
for (name in stmt.bindings) {
if (name != null) {
exports[name] = ModuleExportDefinition(
name = name,
access = stmt.access,
type = JetType.TUnknown,
isReadOnly = stmt.isConst || stmt.access == AccessModifier.PROTECTED,
availableAfterDeclaration = false,
)
}
}
}
else -> Unit
}
}
Expand Down
7 changes: 5 additions & 2 deletions src/main/kotlin/dev/jetpack/script/ScriptRunner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -200,8 +200,11 @@ class ScriptRunner(private val plugin: JetpackPlugin) {
is Statement.CommandDecl -> Unit
else -> {
interpreter.executeStmt(stmt, scope)
if (stmt is Statement.VarDecl) {
module.initializedExports += stmt.name
when (stmt) {
is Statement.VarDecl -> module.initializedExports += stmt.name
is Statement.ObjectDestructuring -> module.initializedExports += stmt.bindings.map { it.localName }
is Statement.ListDestructuring -> module.initializedExports += stmt.bindings.filterNotNull()
else -> Unit
}
}
}
Expand Down