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
125 changes: 80 additions & 45 deletions src/main/kotlin/dev/jetpack/engine/parser/Parser.kt
Original file line number Diff line number Diff line change
Expand Up @@ -258,17 +258,45 @@ class Parser(private val tokens: List<Token>) {
TokenType.KW_COMMAND -> parseCommandDecl(access ?: AccessModifier.PRIVATE, line, isRoot = true)
TokenType.KW_CONST -> {
advance()
if (peekType() == TokenType.LBRACE || peekType() == TokenType.LBRACKET) {
parseDestructuring(access ?: AccessModifier.PRIVATE, isConst = true, line)
if (peekType() == TokenType.LPAREN) {
parseDeconstruction(
access = access ?: AccessModifier.PRIVATE,
isConst = true,
isDeclaration = true,
allowTypes = true,
requireTypes = false,
line = line,
)
} else if (!isTypeKeyword(peekType())) {
throw ParseException("Const can only be used with variable declarations", line)
} else {
parseVarDecl(access ?: AccessModifier.PRIVATE, isConst = true, line)
}
}
else -> {
if ((peekType() == TokenType.LBRACE || peekType() == TokenType.LBRACKET) && isDestructuringAhead()) {
parseDestructuring(access ?: AccessModifier.PRIVATE, isConst = false, line)
if (peekType() == TokenType.KW_VAR && peekType(1) == TokenType.LPAREN) {
advance()
parseDeconstruction(
access = access ?: AccessModifier.PRIVATE,
isConst = false,
isDeclaration = true,
allowTypes = false,
requireTypes = false,
line = line,
)
} else if (peekType() == TokenType.LPAREN && isDeconstructionAhead()) {
val isDeclaration = isTypedDeconstructionPattern()
if (access != null && !isDeclaration) {
throw ParseException("Access modifier can only be used with declarations", line)
}
parseDeconstruction(
access = access ?: AccessModifier.PRIVATE,
isConst = false,
isDeclaration = isDeclaration,
allowTypes = isDeclaration,
requireTypes = isDeclaration,
line = line,
)
} else if (access != null && !isTypeKeyword(peekType())) {
throw ParseException("Access modifier can only be used with top-level declarations", line)
} else if (isTypeKeyword(peekType())) {
Expand Down Expand Up @@ -1090,16 +1118,14 @@ class Parser(private val tokens: List<Token>) {
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
private fun isDeconstructionAhead(): Boolean {
if (peekType() != TokenType.LPAREN) return false
var i = pos + 1
var depth = 0
while (i < tokens.size) {
when (tokens[i].type) {
openType -> depth++
closeType -> {
TokenType.LPAREN -> depth++
TokenType.RPAREN -> {
if (depth == 0) {
i++
while (i < tokens.size && (tokens[i].type == TokenType.NEWLINE || tokens[i].type == TokenType.SEMICOLON)) i++
Expand All @@ -1115,50 +1141,59 @@ class Parser(private val tokens: List<Token>) {
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 isTypedDeconstructionPattern(): Boolean {
if (peekType() != TokenType.LPAREN) return false
var i = pos + 1
while (i < tokens.size && tokens[i].type in setOf(TokenType.NEWLINE, TokenType.SEMICOLON)) i++
return i < tokens.size && isTypeKeyword(tokens[i].type)
}

private fun parseObjectDestructuring(access: AccessModifier, isConst: Boolean, line: Int): Statement.ObjectDestructuring {
expect(TokenType.LBRACE, "Expected '{'")
private fun parseDeconstruction(
access: AccessModifier,
isConst: Boolean,
isDeclaration: Boolean,
allowTypes: Boolean,
requireTypes: Boolean,
line: Int,
): Statement.Deconstruction {
expect(TokenType.LPAREN, "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)) {
val bindings = mutableListOf<DeconstructionBinding>()
while (!check(TokenType.RPAREN) && !isAtEnd()) {
bindings += parseDeconstructionBinding(isDeclaration, allowTypes, requireTypes)
skipNewlines()
if (check(TokenType.COMMA)) {
advance()
expect(TokenType.IDENTIFIER, "Expected local variable name after ':' in object destructuring").value
skipNewlines()
} else {
fieldName
break
}
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")
expect(TokenType.RPAREN, "Expected ')' to close deconstruction pattern")
expect(TokenType.EQ, "Expected '=' after deconstruction pattern")
val initializer = parseExpression()
return Statement.ObjectDestructuring(access, isConst, bindings, initializer, line)
return Statement.Deconstruction(access, isConst, isDeclaration, 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() }
private fun parseDeconstructionBinding(
isDeclaration: Boolean,
allowTypes: Boolean,
requireTypes: Boolean,
): DeconstructionBinding {
if (check(TokenType.IDENTIFIER) && peek().value == "_") {
advance()
return DeconstructionBinding(null, null)
}
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)
if (!allowTypes && isTypeKeyword(peekType())) {
throw ParseException("Deconstruction declared with 'var' cannot specify item types", peek().line)
}
if (requireTypes && !isTypeKeyword(peekType())) {
throw ParseException("Expected typed deconstruction item", peek().line)
}
val typeRef = if (isDeclaration && allowTypes && isTypeKeyword(peekType())) {
parseTypeRef()
} else null
val name = expect(TokenType.IDENTIFIER, "Expected variable name or '_' in deconstruction pattern").value
return DeconstructionBinding(name, typeRef)
}
}
18 changes: 6 additions & 12 deletions src/main/kotlin/dev/jetpack/engine/parser/ast/Statement.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,17 +84,11 @@ sealed class Statement {
val annotations: CommandAnnotations,
override val line: Int,
) : Statement()
data class ObjectDestructuring(
data class Deconstruction(
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 isDeclaration: Boolean,
val bindings: List<DeconstructionBinding>,
val initializer: Expression,
override val line: Int,
) : Statement()
Expand Down Expand Up @@ -146,9 +140,9 @@ data class TypeRef(
val typeArgRef: TypeRef? = null,
)

data class DestructuringBinding(
val fieldName: String,
val localName: String,
data class DeconstructionBinding(
val name: String?,
val typeName: TypeRef?,
)

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

is Statement.ObjectDestructuring -> {
is Statement.Deconstruction -> {
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)
for (binding in stmt.bindings) {
val name = binding.name ?: continue
if (stmt.isDeclaration) {
declare(name, stmt.line)
} else if (!isDeclared(name)) {
error("Undefined identifier '$name'", stmt.line)
}
}
}
}
}
Expand Down Expand Up @@ -347,6 +349,9 @@ class NameResolver(private val reservedNames: Set<String> = emptySet()) {
current[name] = line
}

private fun isDeclared(name: String): Boolean =
scopes.any { name in it }

private fun error(message: String, line: Int) {
errors.add(ResolverError(message, line))
}
Expand Down
59 changes: 43 additions & 16 deletions src/main/kotlin/dev/jetpack/engine/resolver/TypeChecker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -238,24 +238,52 @@ 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.Deconstruction -> {
val positionalTypes = (stmt.initializer as? Expression.ListLiteral)?.elements?.map(::inferExpr)
val initType = if (positionalTypes != null) {
JetType.TList(if (positionalTypes.isEmpty()) JetType.TUnknown else commonSupertype(positionalTypes))
} else {
inferExpr(stmt.initializer)
}
}

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))
errors.add(TypeCheckerError("Deconstruction 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)
for ((index, binding) in stmt.bindings.withIndex()) {
val name = binding.name ?: continue
val bindingElementType = positionalTypes?.getOrNull(index) ?: elementType
if (positionalTypes != null && index >= positionalTypes.size) {
errors.add(TypeCheckerError("Deconstruction index $index is out of range", stmt.line))
}
val declaredType = binding.typeName?.let {
resolveTypeRef(it, stmt.line, "Deconstruction variable '$name'")
}
if (declaredType != null &&
declaredType != JetType.TUnknown &&
bindingElementType != JetType.TUnknown &&
!declaredType.accepts(bindingElementType)
) {
errors.add(TypeCheckerError(
"Deconstruction variable '$name' has type '$declaredType' but list yields '$bindingElementType'",
stmt.line,
))
}
if (stmt.isDeclaration) {
defineType(name, declaredType ?: bindingElementType, stmt.line, stmt.isConst)
} else {
val targetType = lookupType(name) ?: JetType.TUnknown
when {
isConst(name) -> errors.add(TypeCheckerError("Cannot reassign const variable '$name'", stmt.line))
isReadOnly(name) -> errors.add(TypeCheckerError("Cannot reassign foreach item '$name'", stmt.line))
targetType != JetType.TUnknown &&
bindingElementType != JetType.TUnknown &&
!targetType.accepts(bindingElementType) ->
errors.add(TypeCheckerError(
"Cannot assign list element of type '$bindingElementType' to variable '$name' of type '$targetType'",
stmt.line,
))
}
}
}
}

Expand Down Expand Up @@ -1132,8 +1160,7 @@ class TypeChecker(private val typeProvider: BuiltinTypeProvider? = null) {
is Statement.IntervalDecl,
is Statement.ListenerDecl,
is Statement.CommandDecl,
is Statement.ObjectDestructuring,
is Statement.ListDestructuring -> setOf(FlowSignal.FALLTHROUGH)
is Statement.Deconstruction -> setOf(FlowSignal.FALLTHROUGH)
}

private fun analyzeTryFlow(stmt: Statement.TryStmt): Set<FlowSignal> {
Expand Down
35 changes: 12 additions & 23 deletions src/main/kotlin/dev/jetpack/engine/runtime/Interpreter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -296,38 +296,27 @@ 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 -> {
is Statement.Deconstruction -> {
val list = evalExpr(stmt.initializer, scope)
if (list !is JList) throw RuntimeError(
"List destructuring requires a list, got '${list.typeName()}'",
"Deconstruction requires a list, got '${list.typeName()}'",
stmt.line, "TypeException",
)
for ((index, name) in stmt.bindings.withIndex()) {
if (name == null) continue
for ((index, binding) in stmt.bindings.withIndex()) {
val name = binding.name ?: continue
val value = list.elements.getOrNull(index)
?: throw RuntimeError(
"List destructuring index $index is out of range (list has ${list.elements.size} elements)",
"Deconstruction index $index is out of range (list has ${list.elements.size} elements)",
stmt.line, "IndexException",
)
val declaredType = binding.typeName?.toJetTypeOrNull()
val coerced = coerceValueToType(value, declaredType)
withScopeRuntimeError(stmt.line) {
scope.defineCoerced(name, value, stmt.isConst, null)
if (stmt.isDeclaration) {
scope.defineCoerced(name, coerced, stmt.isConst, declaredType)
} else {
scope.set(name, coerced)
}
}
}
}
Expand Down
20 changes: 5 additions & 15 deletions src/main/kotlin/dev/jetpack/script/ScriptModuleFactory.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,24 +79,14 @@ 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) {
is Statement.Deconstruction -> {
if (stmt.isDeclaration) {
for (binding in stmt.bindings) {
val name = binding.name ?: continue
exports[name] = ModuleExportDefinition(
name = name,
access = stmt.access,
type = JetType.TUnknown,
type = binding.typeName?.toJetType() ?: JetType.TUnknown,
isReadOnly = stmt.isConst || stmt.access == AccessModifier.PROTECTED,
availableAfterDeclaration = false,
)
Expand Down
Loading