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

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions Documentation/ArchitectureDecisions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ describes the intended design even when its implementation is still planned.
- **Planned**: no production slice of the decision has shipped.
- **Partial (foundation shipped)**: a tested production slice has shipped, but
the ADR's own implementation checklist still has open requirements.
- **Partial (worktree foundation; not released)**: a tested implementation is
available in an isolated worktree, with remaining ADR requirements open.
- **Implemented**: every normative requirement in the ADR is shipped and its
validation is recorded.

Expand All @@ -32,6 +34,7 @@ describes the intended design even when its implementation is still planned.
| [ADR-0008](0008-adascript-projects-on-ipados.md) | Accepted | Partial (project foundation shipped) | Portable AdaScript projects, iPadOS runtime sessions, Files/iCloud, and Git ownership |
| [ADR-0009](0009-adascript-runtime-configuration.md) | Accepted | Partial (foundation shipped) | Declarative entry plans, plugin presets, typed settings, and runtime-window configuration |
| [ADR-0010](0010-adascript-native-adaui-extension-registry.md) | Accepted | Planned | Versioned descriptors and host factories for native AdaUI views and modifiers used by AdaScript |
| [ADR-0015](0015-adascript-async-tasks-and-coroutines.md) | Accepted | Partial (worktree foundation; not released) | Structured AdaScript async functions, awaitables, task ownership, timers, background I/O, and safe coroutine resumption |

## Multiplayer decisions

Expand Down
2 changes: 1 addition & 1 deletion Documentation/DocCTheme/theme.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"use strict";

const keywords = new Set([
"_args", "_func", "and", "break", "case", "class", "const", "continue", "default", "else", "enum", "event", "extern", "false",
"_args", "_func", "and", "async", "await", "break", "case", "class", "const", "continue", "default", "else", "enum", "event", "extern", "false",
"file", "for", "func", "if", "import", "in", "internal", "is", "lazy", "module", "not", "null", "or", "private", "public", "repeat",
"return", "static", "struct", "super", "switch", "true", "undefined", "var", "while"
]);
Expand Down
5 changes: 5 additions & 0 deletions Editor/Sources/GravityLanguageCore/GravityBuiltins.swift
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,8 @@ enum GravityBuiltins {
static let globalCandidates: [GravityCompletionCandidate] =
[
GravityCompletionCandidate(detail: "Function declaration", insertText: "func name() {\n \n}", kind: .snippet, label: "func", sortText: "10"),
GravityCompletionCandidate(detail: "Suspending function declaration", insertText: "async func name() {\n \n}", kind: .snippet, label: "async func", sortText: "10"),
GravityCompletionCandidate(detail: "Wait for an async task", insertText: "await ", kind: .keyword, label: "await", sortText: "10"),
GravityCompletionCandidate(detail: "Class declaration", insertText: "class Name {\n \n}", kind: .snippet, label: "class", sortText: "11"),
GravityCompletionCandidate(detail: "Variable declaration", insertText: "var ", kind: .keyword, label: "var", sortText: "12"),
GravityCompletionCandidate(detail: "Return statement", insertText: "return ", kind: .keyword, label: "return", sortText: "13"),
Expand All @@ -148,6 +150,9 @@ enum GravityBuiltins {
GravityCompletionCandidate(detail: "Flexible AdaUI space", insertText: "Spacer()", kind: .class, label: "Spacer", sortText: "18"),
GravityCompletionCandidate(detail: "AdaUI divider", insertText: "Divider()", kind: .class, label: "Divider", sortText: "18"),
GravityCompletionCandidate(detail: "AdaEngine asset manager", insertText: "Assets", kind: .variable, label: "Assets", sortText: "18"),
GravityCompletionCandidate(detail: "AdaScript task scheduler", insertText: "Tasks", kind: .class, label: "Tasks", sortText: "18"),
GravityCompletionCandidate(detail: "AdaScript timers", insertText: "Time", kind: .class, label: "Time", sortText: "18"),
GravityCompletionCandidate(detail: "Background save operations", insertText: "Saves", kind: .class, label: "Saves", sortText: "18"),
GravityCompletionCandidate(
detail: "Three-dimensional vector",
insertText: "Vector3(0, 0, 0)",
Expand Down
14 changes: 11 additions & 3 deletions Editor/Sources/GravityLanguageCore/GravityDocumentAnalyzer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,8 @@ struct GravityDocumentAnalyzer {
if let symbol = declarationSymbol(
keyword: token.text,
nameToken: tokens[nameIndex],
memberContext: memberContext
memberContext: memberContext,
isAsync: token.text == "func" && index > 0 && tokens[index - 1].text == "async"
) {
symbols.append(symbol)
}
Expand All @@ -186,13 +187,20 @@ struct GravityDocumentAnalyzer {
return symbols
}

private static func declarationSymbol(keyword: String, nameToken: GravityToken, memberContext: Bool) -> GravitySymbol? {
private static func declarationSymbol(
keyword: String,
nameToken: GravityToken,
memberContext: Bool,
isAsync: Bool
) -> GravitySymbol? {
let kind: GravitySymbolKind
let detail: String
switch keyword {
case "func":
kind = memberContext ? .method : .function
detail = memberContext ? "AdaScript method" : "AdaScript function"
detail = isAsync
? (memberContext ? "AdaScript async method" : "AdaScript async function")
: (memberContext ? "AdaScript method" : "AdaScript function")
case "var":
kind = memberContext ? .property : .variable
detail = memberContext ? "AdaScript property" : "AdaScript variable"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ enum GravitySemanticAnalyzer {
}

private static let keywords: Set<String> = [
"break", "case", "class", "const", "continue", "else", "enum", "event", "extern", "false", "for", "func", "if", "import", "in", "null",
"async", "await", "break", "case", "class", "const", "continue", "else", "enum", "event", "extern", "false", "for", "func", "if", "import", "in", "null",
"private", "public", "repeat", "return", "static", "struct", "switch", "true", "var", "while",
]
}
10 changes: 10 additions & 0 deletions Editor/Tests/AdaEditorTests/GravityLanguageSemanticTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ import Testing

@Suite("AdaScript semantic language features")
struct GravityLanguageSemanticTests {
@Test("Async declarations remain navigable in AdaScript")
func asyncFunctionsAreRecognized() {
let service = GravityLanguageService()
let source = "async func requestConfirmation() { var answer = await wait_confirmation(); }"
let analysis = service.analyze(text: source)
#expect(analysis.symbols.contains { $0.name == "requestConfirmation" && $0.detail == "AdaScript async function" })
let completions = service.completions(text: "as", position: GravitySourcePosition(line: 0, utf16Column: 2))
#expect(completions.contains { $0.label == "async func" })
}

@Test("Annotated lifecycle parameters expose typed host APIs")
func annotatedLifecycleCompletion() {
let service = GravityLanguageService(hostConstructors: [
Expand Down
5 changes: 2 additions & 3 deletions Package.resolved

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1300,7 +1300,7 @@ let package = Package(
package.dependencies += [
.package(
url: "https://github.com/AdaEngine/gravity-lang.git",
exact: "0.9.9"
revision: "24695757a0ba5638b3633004a2166b7878c116de"
),
.package(url: "https://github.com/apple/swift-collections", from: "1.3.0"),
.package(url: "https://github.com/apple/swift-log", from: "1.8.0"),
Expand Down
27 changes: 17 additions & 10 deletions Sources/AdaScriptCompilerCore/AdaScriptAssetsLowerer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,20 @@ public enum AdaScriptAssetsLowerer {
var typedCalls: [Int: (typeName: String, annotationRange: Range<Int>)] = [:]

for index in tokens.indices where tokens[index].text == "var" {
let assetStart = tokens.indices.contains(index + 5) && tokens[index + 5].text == "await" ? index + 6 : index + 5
guard
tokens.indices.contains(index + 8),
tokens.indices.contains(assetStart + 3),
tokens[index + 2].text == ":",
tokens[index + 3].kind == .identifier,
tokens[index + 4].text == "=",
tokens[index + 5].text == "Assets",
tokens[index + 6].text == ".",
["load", "preload"].contains(tokens[index + 7].text),
tokens[index + 8].text == "("
tokens[assetStart].text == "Assets",
tokens[assetStart + 1].text == ".",
["load", "preload", "loadAsync"].contains(tokens[assetStart + 2].text),
tokens[assetStart + 3].text == "("
else {
continue
}
typedCalls[index + 5] = (
typedCalls[assetStart] = (
typeName: tokens[index + 3].text,
annotationRange: tokens[index + 2].startOffset..<tokens[index + 3].endOffset
)
Expand All @@ -37,31 +38,37 @@ public enum AdaScriptAssetsLowerer {
guard
tokens.indices.contains(index + 3),
tokens[index + 1].text == ".",
["load", "preload", "save"].contains(tokens[index + 2].text),
["load", "preload", "save", "loadAsync", "saveAsync"].contains(tokens[index + 2].text),
tokens[index + 3].text == "(",
let closingIndex = matchingClosingParenthesis(openingAt: index + 3, tokens: tokens)
else {
continue
}
let operation: String
let isAsync = tokens[index + 2].text.hasSuffix("Async")
if let typed = typedCalls[index] {
operation = "\"loadTyped\", \"\(typed.typeName)\", "
operation = "\"\(isAsync ? "loadTypedAsync" : "loadTyped")\", \"\(typed.typeName)\", "
replacements.append(
Replacement(endOffset: typed.annotationRange.upperBound, source: "", startOffset: typed.annotationRange.lowerBound)
)
} else {
operation = tokens[index + 2].text == "save" ? "\"save\", " : "\"load\", "
operation = "\"\(isAsync ? tokens[index + 2].text : (tokens[index + 2].text == "save" ? "save" : "load"))\", "
}
replacements.append(
Replacement(
endOffset: tokens[index + 3].endOffset,
source: "__adaAssets.perform([\(operation)",
source: isAsync ? "__adaTaskFromOperation(__adaAssets.begin([\(operation)" : "__adaAssets.perform([\(operation)",
startOffset: tokens[index].startOffset
)
)
replacements.append(
Replacement(endOffset: tokens[closingIndex].startOffset, source: "]", startOffset: tokens[closingIndex].startOffset)
)
if isAsync {
replacements.append(
Replacement(endOffset: tokens[closingIndex].endOffset, source: ")", startOffset: tokens[closingIndex].endOffset)
)
}
}

var result = characters
Expand Down
126 changes: 126 additions & 0 deletions Sources/AdaScriptCompilerCore/AdaScriptAsyncDeclarationScanner.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
public struct AdaScriptAsyncSyntaxError: Error, Sendable, Equatable, CustomStringConvertible {
public let path: String
public let line: Int
public let message: String

public var description: String { "\(path):\(line): \(message)" }
}

public struct AdaScriptAsyncDeclaration: Equatable, Sendable {
public let name: String
public let ownerType: String?
public let line: Int
}

/// Collects AdaEngine callback and suspension metadata. Gravity owns the
/// async grammar, AST lowering, and call effect diagnostics.
public enum AdaScriptAsyncDeclarationScanner {
private enum Scope {
case type(String)
case other
}

public static func declarations(
in source: String,
path: String,
nonSendableTypes: Set<String> = []
) throws -> [AdaScriptAsyncDeclaration] {
var lexer = Lexer(source: source)
let tokens = lexer.lex()
let markedTypes = try nonSendableTypes.union(AdaScriptNonSendableTypes.declared(in: source, path: path))
var result: [AdaScriptAsyncDeclaration] = []

for index in tokens.indices where tokens[index].text == "async" {
let token = tokens[index]
guard tokens.indices.contains(index + 3), tokens[index + 1].text == "func",
tokens[index + 2].kind == .identifier, tokens[index + 3].text == "(",
let close = closing(index + 3, tokens: tokens, open: "(", close: ")") else {
throw error(path, token.line, "expected 'async func name(...)'")
}
let ownerType = try enclosingType(at: index, tokens: tokens, path: path)
if let ownerType, markedTypes.contains(ownerType) {
throw error(path, token.line, "async method captures @nonsendable type '\(ownerType)'")
}
for parameter in parameters(in: tokens[(index + 4)..<close]) {
if let typeName = parameter.typeName, markedTypes.contains(typeName) {
throw error(path, token.line, "async parameter '\(parameter.name)' has @nonsendable type '\(typeName)'")
}
}
result.append(.init(name: tokens[index + 2].text, ownerType: ownerType, line: token.line))
}
return result
}

private static func enclosingType(at index: Int, tokens: [Token], path: String) throws -> String? {
var scopes: [Scope] = []
var segment = 0
for cursor in 0..<index {
switch tokens[cursor].text {
case "{":
let declaration = tokens[segment..<cursor]
if let typeIndex = declaration.firstIndex(where: { ["class", "struct", "enum"].contains($0.text) }),
tokens.indices.contains(typeIndex + 1), tokens[typeIndex + 1].kind == .identifier {
scopes.append(.type(tokens[typeIndex + 1].text))
} else {
scopes.append(.other)
}
segment = cursor + 1
case "}":
guard !scopes.isEmpty else { throw error(path, tokens[cursor].line, "unbalanced braces") }
scopes.removeLast()
segment = cursor + 1
case ";": segment = cursor + 1
default: break
}
}
guard let scope = scopes.last else {
return nil
}
if case let .type(name) = scope {
return name
}
throw error(path, tokens[index].line, "nested async func is not supported")
}

private static func parameters(in tokens: ArraySlice<Token>) -> [(name: String, typeName: String?)] {
guard !tokens.isEmpty else {
return []
}
let items = Array(tokens)
var result: [(name: String, typeName: String?)] = []
var start = 0
var depth = 0
for cursor in 0...items.count {
if cursor < items.count {
if ["(", "[", "{"].contains(items[cursor].text) { depth += 1 }
if [")", "]", "}"].contains(items[cursor].text) { depth -= 1 }
}
guard cursor == items.count || (items[cursor].text == "," && depth == 0) else { continue }
if start < cursor, items[start].kind == .identifier {
let typeIndex = items[start..<cursor].firstIndex(where: { $0.text == ":" }).map { $0 + 1 }
let typeName = typeIndex.flatMap { $0 < cursor && items[$0].kind == .identifier ? items[$0].text : nil }
result.append((items[start].text, typeName))
}
start = cursor + 1
}
return result
}

private static func closing(_ opening: Int, tokens: [Token], open: String, close: String) -> Int? {
var depth = 0
for index in opening..<tokens.count {
if tokens[index].text == open { depth += 1 }
if tokens[index].text == close {
depth -= 1
if depth == 0 {
return index
}
}
}
return nil
}

private static func error(_ path: String, _ line: Int, _ message: String) -> AdaScriptAsyncSyntaxError {
.init(path: path, line: line, message: message)
}
}
46 changes: 46 additions & 0 deletions Sources/AdaScriptCompilerCore/AdaScriptNonSendableTypes.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/// Suspension policy declared at the AdaScript type, independent of parameter names.
public enum AdaScriptNonSendableTypes {
/// Returns classes, structs, and enums annotated with `@nonsendable`.
public static func declared(in source: String, path: String) throws -> Set<String> {
var lexer = Lexer(source: source)
let tokens = lexer.lex()
var names = Set<String>()

for index in tokens.indices where tokens[index].text == "@" {
guard tokens.indices.contains(index + 1), tokens[index + 1].text == "nonsendable" else {
continue
}
var cursor = index + 2
while tokens.indices.contains(cursor), tokens[cursor].text == "@" {
cursor += 2
if tokens.indices.contains(cursor), tokens[cursor].text == "(" {
guard let end = closingParenthesis(at: cursor, tokens: tokens) else {
throw AdaScriptAsyncSyntaxError(path: path, line: tokens[index].line, message: "unterminated annotation after @nonsendable")
}
cursor = end + 1
}
}
guard tokens.indices.contains(cursor + 1),
["class", "struct", "enum"].contains(tokens[cursor].text),
tokens[cursor + 1].kind == .identifier else {
throw AdaScriptAsyncSyntaxError(path: path, line: tokens[index].line, message: "@nonsendable must annotate a type declaration")
}
names.insert(tokens[cursor + 1].text)
}
return names
}

private static func closingParenthesis(at opening: Int, tokens: [Token]) -> Int? {
var depth = 0
for index in opening..<tokens.count {
if tokens[index].text == "(" { depth += 1 }
if tokens[index].text == ")" {
depth -= 1
if depth == 0 {
return index
}
}
}
return nil
}
}
Loading
Loading