diff --git a/Makefile b/Makefile index 0556129..a9804b7 100644 --- a/Makefile +++ b/Makefile @@ -85,9 +85,13 @@ lib: $(OBJ) staticlib: $(OBJ) $(AR) rcs $(SLIBTARGET) $(OBJ) +# Native archive + standalone host + VM differential coverage. +aottest: gravity + python3 test/aot/run_all.py + clean: rm -f $(OBJ) $(GRAVITY_OBJ) $(EXAMPLE_OBJ) $(JSONTEST_OBJ) $(DEP) gravity example jsontest libgravity.dylib libgravity.so $(SLIBTARGET) gravity.dll -.PHONY: all clean lib staticlib +.PHONY: all clean lib staticlib aottest -include $(DEP) diff --git a/Package.swift b/Package.swift index 3fbe699..6d14f83 100644 --- a/Package.swift +++ b/Package.swift @@ -13,6 +13,8 @@ let package = Package( .visionOS(.v1) ], products: [ + .library(name: "GravityAOT", targets: ["GravityAOT"]), + .library(name: "CGravity", targets: ["CGravity"]), .library( name: "Gravity", targets: ["Gravity"] @@ -22,6 +24,7 @@ let package = Package( .package(url: "https://github.com/swiftlang/swift-syntax", from: "602.0.0") ], targets: [ + .target(name: "GravityAOT", dependencies: ["CGravity"], path: "binding/GravityAOT"), .executableTarget( name: "GravitySwiftExample", dependencies: ["Gravity"], diff --git a/README.md b/README.md index 8cf101d..75234f9 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,10 @@ Requires a C99 compiler. No external dependencies. ## Usage +Experimental native AOT: `./gravity --emit-c source.gravity --module gameplay -o gameplay.c`. +See [AOT backend and static library builds](docs/AOT.md) for native classes/structs, attributes, +standalone C ABI, cross-compilation and validation. + ```bash ./gravity file.gravity # Compile and execute a source file ./gravity -c file.gravity # Compile to bytecode (outputs gravity.g) diff --git a/binding/GravityAOT/NativeMetadata.swift b/binding/GravityAOT/NativeMetadata.swift new file mode 100644 index 0000000..eeb4028 --- /dev/null +++ b/binding/GravityAOT/NativeMetadata.swift @@ -0,0 +1,133 @@ +import CGravity + +public indirect enum NativeLiteral: Equatable, Sendable { + case null, boolean(Bool), integer(Int64), double(Double), string(String), identifier(String), list([NativeLiteral]) + + public var string: String? { + switch self { case let .string(value), let .identifier(value): value; default: nil } + } + public var identifiers: [String] { + switch self { + case let .identifier(value): [value] + case let .list(values): values.compactMap { if case let .identifier(v) = $0 { v } else { nil } } + default: [] + } + } +} + +public struct NativeAttribute: Equatable, Sendable { + public struct Argument: Equatable, Sendable { + public let label: String? + public let value: NativeLiteral + } + public let name: String + public let arguments: [Argument] + public let fileID: UInt32 + public let line: UInt32 + public let column: UInt32 + + public func argument(_ label: String) -> NativeLiteral? { arguments.first { $0.label == label }?.value } + public var positional: [NativeLiteral] { arguments.filter { $0.label == nil }.map(\.value) } +} + +public struct NativeDeclaration: Equatable, Sendable { + public enum Kind: UInt32, Sendable { case function, `class`, `struct`, field, method } + public struct Parameter: Equatable, Sendable { public let name: String; public let type: String? } + public let name: String + public let parent: String? + public let type: String? + public let kind: Kind + public let attributes: [NativeAttribute] + public let parameters: [Parameter] + public let line: UInt32 + + public func attribute(_ name: String) -> NativeAttribute? { attributes.first { $0.name == name } } +} + +public struct NativeType: Equatable, Sendable { + public struct Field: Equatable, Sendable { + public let declaration: NativeDeclaration + public let defaultValue: NativeLiteral + public let isReadOnly: Bool + } + public let declaration: NativeDeclaration + public let fields: [Field] + public let methods: [NativeDeclaration] + public var name: String { declaration.name } + public var isStruct: Bool { declaration.kind == .struct } +} + +public enum NativeRuntimeError: Error, Equatable, Sendable { + case incompatibleABI(UInt32) + case malformedMetadata(String) + case unknownType(String) + case foreignInstance + case runtime(code: UInt32, detail: String?) + case allocationLimit + case unsupportedValue + case reentrantCall +} + +func nativeString(_ pointer: UnsafePointer?) throws -> String { + guard let pointer else { throw NativeRuntimeError.malformedMetadata("Missing name") } + return String(cString: pointer) +} +func nativeArray(_ pointer: UnsafePointer?, _ count: UInt32) throws -> UnsafeBufferPointer { + guard count < 1_000_000, count == 0 || pointer != nil else { + throw NativeRuntimeError.malformedMetadata("Invalid array count or pointer") + } + return UnsafeBufferPointer(start: pointer, count: Int(count)) +} +func nativeAttributeValue(_ p: UnsafePointer, depth: Int = 0) throws -> NativeLiteral { + guard depth < 64 else { throw NativeRuntimeError.malformedMetadata("Attribute nesting exceeds 64") } + let value = p.pointee + switch value.kind { + case 0: return .identifier(try nativeString(value.text)) + case 1: return .string(try nativeString(value.text)) + case 2: return .integer(value.integer) + case 3: return .double(value.floating) + case 4: return .boolean(value.integer != 0) + case 5: return .null + case 6: + return .list(try nativeArray(value.items, value.count).map { + guard let item = $0 else { throw NativeRuntimeError.malformedMetadata("Null attribute item") } + return try nativeAttributeValue(item, depth: depth + 1) + }) + default: throw NativeRuntimeError.malformedMetadata("Unknown attribute value kind") + } +} +func nativeDeclaration(_ p: UnsafePointer) throws -> NativeDeclaration { + let d = p.pointee + guard let kind = NativeDeclaration.Kind(rawValue: d.kind) else { + throw NativeRuntimeError.malformedMetadata("Unknown declaration kind") + } + let attributes = try nativeArray(d.attributes, d.attribute_count).map { a in + NativeAttribute( + name: try nativeString(a.name), + arguments: try nativeArray(a.arguments, a.argument_count).map { arg in + guard let value = arg.value else { throw NativeRuntimeError.malformedMetadata("Null attribute argument") } + return NativeAttribute.Argument(label: arg.label.map { String(cString: $0) }, value: try nativeAttributeValue(value)) + }, + fileID: a.fileid, line: a.line, column: a.column + ) + } + return NativeDeclaration( + name: try nativeString(d.name), parent: d.parent.map { String(cString: $0) }, type: d.type_name.map { String(cString: $0) }, + kind: kind, attributes: attributes, + parameters: try nativeArray(d.parameters, d.parameter_count).map { + NativeDeclaration.Parameter(name: try nativeString($0.name), type: $0.type_name.map { String(cString: $0) }) + }, line: d.line + ) +} +func nativeLiteral(_ value: gravity_aot_value) throws -> NativeLiteral { + switch value.kind { + case 0: .null + case 1: .integer(value.integer) + case 2: .boolean(value.integer != 0) + case 3: .double(value.floating) + case 4: + if let p = value.string { .string(String(decoding: UnsafeRawBufferPointer(start: p, count: Int(value.length)), as: UTF8.self)) } + else { throw NativeRuntimeError.malformedMetadata("Null string") } + default: throw NativeRuntimeError.unsupportedValue + } +} diff --git a/binding/GravityAOT/NativeModule.swift b/binding/GravityAOT/NativeModule.swift new file mode 100644 index 0000000..5346697 --- /dev/null +++ b/binding/GravityAOT/NativeModule.swift @@ -0,0 +1,246 @@ +import CGravity +import Foundation + +/// Owns native memory and keeps module code alive. A recursive lock serializes +/// every C invocation and host callback. Instances retain their module, and the +/// module retains no Swift instance handles, avoiding an ownership cycle. +public final class NativeModule: @unchecked Sendable { + public let declarations: [NativeDeclaration] + public let types: [NativeType] + private let lock = NSRecursiveLock() + private let module: UnsafePointer + private let codeOwner: AnyObject? + private let typePointers: [String: UnsafePointer] + private let allocationLimit: Int + private let fuelPerCall: UInt64 + private let context: UnsafeMutablePointer + private var bridge = gravity_aot_host() + private var allocations: [UnsafeMutableRawPointer] = [] + private var allocatedBytes = 0 + private var handles: [UnsafeMutableRawPointer: any NativeHostObject] = [:] + private var active = false + private var hostError: String? + + /// `module` must point to valid trusted metadata generated by Gravity. + /// Retain a dynamic-library owner here when the module is not statically linked. + public init( + module: UnsafePointer, retaining owner: AnyObject? = nil, + allocationLimit: Int = 64 * 1024 * 1024, fuelPerCall: UInt64 = 1_000_000 + ) throws { + guard module.pointee.abi_version == GRAVITY_AOT_ABI_VERSION else { + throw NativeRuntimeError.incompatibleABI(module.pointee.abi_version) + } + guard allocationLimit > 0, fuelPerCall > 0 else { throw NativeRuntimeError.allocationLimit } + self.module = module; self.codeOwner = owner; self.allocationLimit = allocationLimit; self.fuelPerCall = fuelPerCall + declarations = try nativeArray(module.pointee.declarations, module.pointee.declaration_count).map { + // The buffer points at immutable metadata for the module lifetime. + var copy = $0 + return try withUnsafePointer(to: ©, nativeDeclaration) + } + let rawTypes = try nativeArray(module.pointee.types, module.pointee.type_count) + var pointers: [String: UnsafePointer] = [:] + var values: [NativeType] = [] + for (index, type) in rawTypes.enumerated() { + guard let declaration = type.declaration, let base = rawTypes.baseAddress else { + throw NativeRuntimeError.malformedMetadata("Missing type declaration") + } + let d = try nativeDeclaration(declaration) + guard pointers[d.name] == nil else { throw NativeRuntimeError.malformedMetadata("Duplicate type \(d.name)") } + pointers[d.name] = base.advanced(by: index) + let fields = try nativeArray(type.fields, type.field_count).map { field in + guard let declaration = field.declaration else { throw NativeRuntimeError.malformedMetadata("Missing field declaration") } + return NativeType.Field(declaration: try nativeDeclaration(declaration), defaultValue: try nativeLiteral(field.default_value), isReadOnly: field.readonly != 0) + } + let methods = try nativeArray(type.methods, type.method_count).map { method in + guard let declaration = method.declaration, method.call != nil else { + throw NativeRuntimeError.malformedMetadata("Missing method declaration or callback") + } + return try nativeDeclaration(declaration) + } + values.append(NativeType(declaration: d, fields: fields, methods: methods)) + } + types = values; typePointers = pointers + // C invokes Swift callbacks while this context is live. Store it in owned + // C memory, rather than taking an inout access to a Swift stored property. + context = .allocate(capacity: 1) + context.initialize(to: gravity_aot_context_init(0)) + bridge = gravity_aot_host(get: Self.bridgeGet, set: Self.bridgeSet, call: Self.bridgeCall, next: Self.bridgeNext) + } + + deinit { + context.deinitialize(count: 1); context.deallocate() + for pointer in allocations { pointer.deallocate() } + } + + public func makeInstance(type name: String, arguments: [NativeValue] = []) throws -> NativeInstance { + try lock.withLock { + guard let type = typePointers[name] else { throw NativeRuntimeError.unknownType(name) } + return try scope { + let args = try arguments.map(encode) + let raw = args.withUnsafeBufferPointer { gravity_aot_construct(context, type, $0.baseAddress, UInt32($0.count)) } + try check() + return NativeInstance(owner: self, raw: raw, typeName: name) + } + } + } + + @discardableResult + public func invoke( + _ instance: NativeInstance, method: String, arguments: [NativeValue] = [], bindings: [String: NativeValue] = [:] + ) throws -> NativeValue { + try lock.withLock { + try validate(instance) + return try scope { + for (name, value) in bindings { + let raw = try encode(value) + _ = name.withCString { gravity_aot_set(context, instance.raw, $0, raw) } + try check() + } + let args = try arguments.map(encode) + let value = method.withCString { name in + args.withUnsafeBufferPointer { gravity_aot_call(context, instance.raw, name, $0.baseAddress, UInt32($0.count)) } + } + try check() + return try decode(value) + } + } + } + + public func read(_ instance: NativeInstance, field: String) throws -> NativeValue { + try lock.withLock { + try validate(instance) + return try scope { + let value = field.withCString { gravity_aot_get(context, instance.raw, $0) } + try check(); return try decode(value) + } + } + } + + public func write(_ instance: NativeInstance, field: String, value: NativeValue) throws { + try lock.withLock { + try validate(instance) + try scope { + let raw = try encode(value) + _ = field.withCString { gravity_aot_set(context, instance.raw, $0, raw) }; try check() + } + } + } + + private func validate(_ instance: NativeInstance) throws { + guard instance.owner === self else { throw NativeRuntimeError.foreignInstance } + } + private func scope(_ body: () throws -> T) throws -> T { + guard !active else { throw NativeRuntimeError.reentrantCall } + active = true; context.pointee.error = 0; context.pointee.fuel = fuelPerCall; context.pointee.depth = 0 + context.pointee.host_generation &+= 1; hostError = nil + context.pointee.allocate = Self.allocate + context.pointee.allocation_data = Unmanaged.passUnretained(self).toOpaque() + context.pointee.host_data = Unmanaged.passUnretained(self).toOpaque() + defer { + context.pointee.host = nil; handles.removeAll(keepingCapacity: true); active = false + } + return try withUnsafePointer(to: &bridge) { pointer in + context.pointee.host = pointer + return try body() + } + } + private func check() throws { + guard context.pointee.error == 0 else { throw NativeRuntimeError.runtime(code: context.pointee.error, detail: hostError) } + } + private func encode(_ value: NativeValue) throws -> gravity_aot_value { + switch value { + case .null: return gravity_aot_null() + case let .boolean(v): return gravity_aot_bool(v ? 1 : 0) + case let .integer(v): return gravity_aot_int(v) + case let .double(v): return gravity_aot_float(v) + case let .string(v): + let bytes = Array(v.utf8) + guard bytes.count < Int(UInt32.max), let p = allocateBytes(bytes.count + 1) else { throw NativeRuntimeError.allocationLimit } + let chars = p.assumingMemoryBound(to: UInt8.self) + for (i, byte) in bytes.enumerated() { chars[i] = byte }; chars[bytes.count] = 0 + return gravity_aot_string(UnsafeRawPointer(p).assumingMemoryBound(to: CChar.self), UInt32(bytes.count)) + case let .object(v): try validate(v); return v.raw + case let .host(v): + let pointer = Unmanaged.passUnretained(v as AnyObject).toOpaque() + handles[pointer] = v + return gravity_aot_host_ref(context, pointer) + case let .list(v): + guard v.count < Int(UInt32.max) else { throw NativeRuntimeError.unsupportedValue } + let raw = try v.map(encode) + let list = raw.withUnsafeBufferPointer { gravity_aot_list_new(context, $0.baseAddress, UInt32($0.count)) } + try check(); return list + } + } + private func decode(_ raw: gravity_aot_value, depth: Int = 0) throws -> NativeValue { + guard depth < 64 else { throw NativeRuntimeError.unsupportedValue } + switch raw.kind { + case 0...4: return try NativeValue(nativeLiteral(raw)) + case 5: + guard let object = raw.object, let declaration = object.pointee.type?.pointee.declaration else { + throw NativeRuntimeError.unsupportedValue + } + let name = try nativeString(declaration.pointee.name) + guard typePointers[name] == object.pointee.type else { throw NativeRuntimeError.foreignInstance } + return .object(NativeInstance(owner: self, raw: raw, typeName: name)) + case 6: + guard raw.length == context.pointee.host_generation, let p = raw.handle, let host = handles[p] else { + throw NativeRuntimeError.runtime(code: UInt32(GRAVITY_AOT_STALE_HANDLE), detail: nil) + } + return .host(host) + case 7: + guard let list = raw.list else { throw NativeRuntimeError.unsupportedValue } + return .list(try nativeArray(UnsafePointer(list.pointee.items), list.pointee.count).map { try decode($0, depth: depth + 1) }) + default: throw NativeRuntimeError.unsupportedValue + } + } + private func allocateBytes(_ count: Int) -> UnsafeMutableRawPointer? { + guard count > 0, count <= allocationLimit - allocatedBytes else { + hostError = "Native module allocation limit exceeded"; return nil + } + let pointer = UnsafeMutableRawPointer.allocate(byteCount: count, alignment: 16) + allocations.append(pointer); allocatedBytes += count; return pointer + } + private static func owner(_ data: UnsafeMutableRawPointer?) -> NativeModule? { + data.map { Unmanaged.fromOpaque($0).takeUnretainedValue() } + } + private static let allocate: @convention(c) (UnsafeMutableRawPointer?, Int) -> UnsafeMutableRawPointer? = { data, size in + owner(data)?.allocateBytes(size) + } + private func performBridge(_ body: () throws -> Int32) -> Int32 { + do { return try body() } catch { if hostError == nil { hostError = String(describing: error) }; return 0 } + } + private func host(_ value: gravity_aot_value) throws -> any NativeHostObject { + guard case let .host(host) = try decode(value) else { throw NativeRuntimeError.unsupportedValue } + return host + } + private static let bridgeGet: @convention(c) (UnsafeMutableRawPointer?, gravity_aot_value, UnsafePointer?, UnsafeMutablePointer?) -> Int32 = { data, target, key, out in + guard let owner = owner(data), let key, let out else { return 0 } + return owner.performBridge { + guard let value = try owner.host(target).read(String(cString: key)) else { return 0 } + out.pointee = try owner.encode(value); return 1 + } + } + private static let bridgeSet: @convention(c) (UnsafeMutableRawPointer?, gravity_aot_value, UnsafePointer?, gravity_aot_value) -> Int32 = { data, target, key, value in + guard let owner = owner(data), let key else { return 0 } + return owner.performBridge { try owner.host(target).write(String(cString: key), value: owner.decode(value)) ? 1 : 0 } + } + private static let bridgeCall: @convention(c) (UnsafeMutableRawPointer?, gravity_aot_value, UnsafePointer?, UnsafePointer?, UInt32, UnsafeMutablePointer?) -> Int32 = { data, target, name, args, count, out in + guard let owner = owner(data), let name, let out else { return 0 } + return owner.performBridge { + let values = try nativeArray(args, count).map { try owner.decode($0) } + guard let value = try owner.host(target).call(String(cString: name), arguments: values) else { return 0 } + out.pointee = try owner.encode(value); return 1 + } + } + private static let bridgeNext: @convention(c) (UnsafeMutableRawPointer?, gravity_aot_value, UnsafeMutablePointer?, UnsafeMutablePointer?) -> Int32 = { data, target, state, out in + guard let owner = owner(data), let state, let out else { return -1 } + var failed = false + let result = owner.performBridge { + do { + guard let value = try owner.host(target).next(state: &state.pointee) else { return 0 } + out.pointee = try owner.encode(value); return 1 + } catch { failed = true; throw error } + } + return failed ? -1 : result + } +} diff --git a/binding/GravityAOT/NativeValue.swift b/binding/GravityAOT/NativeValue.swift new file mode 100644 index 0000000..8c925ac --- /dev/null +++ b/binding/GravityAOT/NativeValue.swift @@ -0,0 +1,57 @@ +import CGravity + +/// Host capabilities are borrowed for one synchronous invocation. Implementations +/// must invalidate access to engine data after that invocation returns. +public protocol NativeHostObject: AnyObject { + func read(_ key: String) throws -> NativeValue? + func write(_ key: String, value: NativeValue) throws -> Bool + func call(_ method: String, arguments: [NativeValue]) throws -> NativeValue? + func next(state: inout UInt64) throws -> NativeValue? +} +extension NativeHostObject { + public func read(_: String) throws -> NativeValue? { nil } + public func write(_: String, value _: NativeValue) throws -> Bool { false } + public func call(_: String, arguments _: [NativeValue]) throws -> NativeValue? { nil } + public func next(state _: inout UInt64) throws -> NativeValue? { nil } +} + +public indirect enum NativeValue { + case null, boolean(Bool), integer(Int64), double(Double), string(String) + case object(NativeInstance), host(any NativeHostObject), list([NativeValue]) + + public init(_ literal: NativeLiteral) throws { + switch literal { + case .null: self = .null + case let .boolean(v): self = .boolean(v) + case let .integer(v): self = .integer(v) + case let .double(v): self = .double(v) + case let .string(v): self = .string(v) + case let .list(v): self = .list(try v.map(Self.init)) + case .identifier: throw NativeRuntimeError.unsupportedValue + } + } + public var literal: NativeLiteral? { + switch self { + case .null: .null + case let .boolean(v): .boolean(v) + case let .integer(v): .integer(v) + case let .double(v): .double(v) + case let .string(v): .string(v) + case let .list(v): + if v.allSatisfy({ $0.literal != nil }) { .list(v.compactMap(\.literal)) } else { nil } + case .object, .host: nil + } + } +} + +/// Identity and module ownership are immutable. All native storage access is +/// serialized by the owning NativeModule's lock; the handle never exposes C data. +public final class NativeInstance: @unchecked Sendable { + let owner: NativeModule + let raw: gravity_aot_value + public let typeName: String + + init(owner: NativeModule, raw: gravity_aot_value, typeName: String) { + self.owner = owner; self.raw = raw; self.typeName = typeName + } +} diff --git a/binding/GravitySwift/GravityAnnotation.swift b/binding/GravitySwift/GravityAnnotation.swift index 5f482ff..7e680a7 100644 --- a/binding/GravitySwift/GravityAnnotation.swift +++ b/binding/GravitySwift/GravityAnnotation.swift @@ -6,11 +6,19 @@ public struct GravityAnnotation: Sendable, Equatable { public let arguments: [Argument] public let source: SourceLocation + public init(name: String, target: Target, arguments: [Argument], source: SourceLocation) { + self.name = name; self.target = target; self.arguments = arguments; self.source = source + } + public struct Target: Sendable, Equatable { public let kind: Kind public let identifier: String public let parentIdentifier: String? + public init(kind: Kind, identifier: String, parentIdentifier: String? = nil) { + self.kind = kind; self.identifier = identifier; self.parentIdentifier = parentIdentifier + } + public enum Kind: UInt32, Sendable, Equatable { case enumeration = 7 case function = 8 @@ -36,6 +44,10 @@ public struct GravityAnnotation: Sendable, Equatable { public let fileID: UInt32 public let line: UInt32 public let column: UInt32 + + public init(fileID: UInt32, line: UInt32, column: UInt32) { + self.fileID = fileID; self.line = line; self.column = column + } } public indirect enum Value: Sendable, Equatable { diff --git a/docs/AOT.md b/docs/AOT.md new file mode 100644 index 0000000..2bb60bf --- /dev/null +++ b/docs/AOT.md @@ -0,0 +1,193 @@ +# Experimental native AOT backend + +Gravity owns native code generation, the standalone runtime ABI and the library +build tool. Hosts such as AdaEngine consume generated libraries; they do not +implement a second Gravity compiler. + +The backend emits C11 from the checked Gravity AST. The existing host +compiler currently also produces an unused bytecode closure during frontend +validation. The generated library contains native function bodies, not embedded +bytecode, and execution does **not** require the Gravity VM or `libgravity`. + +## Build and consume a library + +```sh +make -j4 +python3 tools/aot_build.py test/aot/scalars.gravity \ + --module gameplay --output /tmp/gameplay/libgameplay.a +``` + +The output directory contains `libgameplay.a`, `gameplay.c`, `gameplay.h` and +`gravity_aot_runtime.h` / `gravity_aot_objects.h`. Include `gameplay.h` and link the archive: + +```c +#include "gameplay.h" +#include + +gravity_aot_context context = gravity_aot_context_init(100000); +const gravity_aot_module *module = gameplay_get_module(); +if (module->abi_version == GRAVITY_AOT_ABI_VERSION) { + for (uint32_t i = 0; i < module->count; ++i) { + if (!strcmp(module->exports[i].name, "main")) { + gravity_aot_value result = module->exports[i].call(&context, NULL, 0); + /* Check context.error before reading result. */ + } + } +} +``` + +To generate C without invoking a target compiler: + +```sh +./gravity --emit-c source.gravity --module gameplay -o gameplay.c +``` + +`.ada` inputs use the same parser; the file extension is unrestricted. + +## Cross compilation + +The Gravity executable runs on the desktop host. Only the emitted C is compiled +for the destination. Pass the target triple and the destination SDK/sysroot to +the build tool; it forwards them to Clang using argument arrays, without a shell: + +```sh +python3 tools/aot_build.py source.gravity --module gameplay \ + --target arm64-apple-ios16.0 \ + --sysroot "$(xcrun --sdk iphoneos --show-sdk-path)" \ + --output /tmp/gameplay-ios/libgameplay.a +``` + +Device and Simulator require separate builds (for example, +`arm64-apple-ios16.0-simulator` with the Simulator SDK). An archive is specific +to an OS, architecture and ABI. It does not package the host engine, sign an app, +create an XCFramework or validate execution on the destination. `--cc`, `--ar` +and repeatable `--cflag=-flag` support explicit toolchain configuration. + +Failed generation or target compilation leaves previously published artifacts +unchanged. Unsupported constructs produce a source-located compiler error and +never fall back to VM execution. + +## Declarations, attributes and native objects + +The experimental standalone ABI is now **version 2**. Rebuild both libraries and +hosts generated with ABI 1. Values represent Int, Bool, Float, null, literal +strings, native objects, lists, ranges and borrowed host handles. Native methods +and metadata live in the same archive; no Gravity VM is linked for execution. + +`module->declarations` records classes/structs, fields, methods and functions, +including source file IDs, line/column, parent names, parameter names/types and +complete attribute arguments. Arguments preserve their labels and kinds: +identifiers, strings, signed Int/Float, Bool, null and recursively nested lists. +Attributes on local declarations are rejected rather than silently omitted. + +Attribute names are host-defined. Gravity preserves `@system`, `@query`, +`@resource`, `@component`, `@export`, `@scriptable`, `@before`/`@after`, `@rpc`, +`@network_command`, `@replicated_component`, `@network_field`, `@local`, `@tool` +and custom annotations through this generic representation. **Preserving +metadata does not implement the host service:** the host must validate attribute +targets/arguments, register schemas, resolve dependencies and choose callbacks. +For example, RPC metadata plus a native method is not a network transport. +AdaScript UI views remain unavailable in AdaEngine; metadata alone does not enable +`@view` rendering or editor tool permissions. + +`module->types` exposes stored field defaults and native method pointers. Hosts +can instantiate a type with `gravity_aot_construct`, bind query/resource fields +with `gravity_aot_set` and invoke `update`, `ready`, `fixedUpdate`, `event` and +`destroy` with `gravity_aot_call`. Methods named `event` are now accepted in class +and struct declarations. Self-field references compile to resolved field slots. +Classes preserve reference identity; assignment/parameter passing copies structs. +Stored field defaults currently must be scalar/string constants or null. + +The host supplies allocation memory. One simple setup is: + +```c +unsigned char memory[32768]; +gravity_aot_arena arena = {memory, sizeof(memory), 0}; +gravity_aot_context context = gravity_aot_context_init(100000); +context.allocate = gravity_aot_arena_allocate; +context.allocation_data = &arena; +``` + +Arena exhaustion reports `GRAVITY_AOT_MEMORY`. There is no garbage collector; +objects, lists and ranges remain valid only while their supplied storage remains +alive. Do not reset an arena while native instances or values still reference it. +Hosts should distinguish persistent instance storage from temporary invocation +storage. Concurrent mutation of a shared instance needs host synchronization. + +## Host bridge and supported execution + +`gravity_aot_host` provides property get/set, method calls and iteration callbacks. +Borrowed handles use `gravity_aot_host_ref(&context, pointer)`. Advance +`context.host_generation` at the end of a borrow scope and refresh bindings before +the next callback. Old handles fail with `GRAVITY_AOT_STALE_HANDLE` when used. +Only the C host can inspect their payload; source code uses ordinary properties +and method calls. Host iteration returns native scalar/object values or borrowed +rows, allowing a query bridge to mutate existing component data without allocating +one native instance per entity. + +Supported execution includes positional function/method calls, constructors, +recursion, local/field/list assignment, numeric arithmetic/comparisons, literal +string comparison, `if`/`else`, `while`, `for` over lists/ranges or host iterables, +`break`/`continue` and `return`. Logical operators are eager, matching Gravity. +Floating `%` uses IEEE `remainder`, matching the VM; consumers need their target's +C math runtime when that operator is used (for example, `-lm` on Linux). + +Native parameter annotations check Int/Bool/Float/String/List kinds and native +class identity. Host handles rely on the host's type contract. The dynamic VM +does not enforce parameter annotations. Contexts store the first error, fuel and +recursion depth. Fuel is consumed on function entry, each loop condition and +object construction; reset after errors. Native calls unwind their depth on error. + +Closures, inheritance, nested/static classes, static/computed properties, maps, +string interpolation/concatenation, named/default arguments, `repeat`, async, +global mutable variables and bodyless function declarations remain unsupported. +The host bridge exposes external globals/functions through declared extern +bindings. Unsupported source constructs produce compile errors; missing bridge +operations and invalid runtime values produce context errors. Compilation and +execution never silently fall back to the VM. + +## Engine-style proof fixture + +```sh +python3 tools/aot_build.py test/aot/attributes.gravity \ + --module attributes --output /tmp/native-attributes/libattributes.a +clang -std=c11 -I/tmp/native-attributes test/aot/attributes_host.c \ + /tmp/native-attributes/libattributes.a -o /tmp/native-attributes/host +/tmp/native-attributes/host +``` + +The fixture exercises component/resource/network/tool metadata, real native +class/struct semantics, query filters and position updates through a C host, +scriptable lifecycle, an RPC-annotated method, UTF-8 field values and stale-borrow +errors. This proves the Gravity backend and generic host ABI. The AdaEngine +adapter/export path has not yet been connected to this ABI. + +## Verification + +```sh +make -j4 +python3 test/aot/run_all.py +./gravity -t test/unittest +``` + +The AOT tests build and link a native archive without `libgravity`, compare its +result against VM execution, instrument generated code with address/undefined +behavior sanitizers, exercise ABI/error/termination cases, and verify that +unsupported source and missing SDKs do not replace existing output. + +## Swift host facade + +The `GravityAOT` SwiftPM product consumes canonical `CGravity` declarations and +provides `NativeModule`, immutable metadata snapshots, `NativeInstance`, +`NativeValue` and `NativeHostObject` callbacks. The facade owns a stable C context +and a bounded allocation pool, protects invocations with a module lock, and +invalidates borrowed host handles between calls. Instances retain their module; +there is no global VM or Swift-instance ownership cycle. For dynamically loaded +code, retain the library owner in the module initializer. + +The AdaEngine adapter can be enabled during development using +`ADAENGINE_GRAVITY_PACKAGE_PATH` pointing at this checkout. It registers native +systems, component/resource schemas and scriptable factories in existing engine +infrastructure. Its native integration tests run actual ECS queries and lifecycle +callbacks against statically linked generated code. World/input/assets/network/UI +adapters and an Editor native export flow remain separate work. diff --git a/src/cli/gravity.c b/src/cli/gravity.c index 456eb7d..678ffb5 100644 --- a/src/cli/gravity.c +++ b/src/cli/gravity.c @@ -7,6 +7,7 @@ // #include "gravity_compiler.h" +#include "gravity_aot.h" #define GRAVITY_INCLUDE_MATH #define GRAVITY_INCLUDE_JSON #define GRAVITY_INCLUDE_ENV @@ -270,6 +271,7 @@ static void print_help (void) { printf(" --version show version information and exit\n"); printf(" --help show command line usage and exit\n"); printf(" -c input_file compile input_file\n"); + printf(" --emit-c input emit experimental AOT C (--module prefix -o output.c)\n"); printf(" -o output_file specify output file name (default to %s)\n", DEFAULT_OUTPUT); printf(" -x input_file execute input_file (JSON format expected)\n"); printf(" -i source_code compile and execute source_code string\n"); @@ -399,7 +401,55 @@ static void gravity_unittest (void) { // MARK: - +static int gravity_emit_c(int argc, const char *argv[]) { + const char *input = NULL, *output = NULL, *prefix = "gravity_aot"; + for (int i = 1; i < argc; ++i) { + const char *option = argv[i]; + if (strcmp(option, "--emit-c") && strcmp(option, "--module") && strcmp(option, "-o")) { + fprintf(stderr, "Unknown AOT option: %s\n", option); return 1; + } + if (++i >= argc) { fprintf(stderr, "Missing value for %s\n", option); return 1; } + if (!strcmp(option, "--emit-c")) { + if (input) { fprintf(stderr, "Duplicate AOT input\n"); return 1; } + input = argv[i]; + } else if (!strcmp(option, "--module")) prefix = argv[i]; + else output = argv[i]; + } + if (!input || !output || !strcmp(input, output)) { + fprintf(stderr, "Usage: gravity --emit-c input.gravity --module prefix -o output.c\n"); return 1; + } + mem_init(); + size_t size = 0; + const char *source = file_read(input, &size); + if (!source || !size) { fprintf(stderr, "Cannot read AOT input: %s\n", input); if (source) mem_free(source); return 1; } + gravity_delegate_t delegate = {.error_callback = report_error, .loadfile_callback = load_file}; + gravity_compiler_t *compiler = gravity_compiler_create(&delegate); + FILE *staged = tmpfile(); + bool ok = compiler && staged; + if (!ok) fprintf(stderr, "Cannot initialize AOT compiler or staging stream\n"); + if (ok) ok = gravity_compiler_run(compiler, source, size, 0, true, true) != NULL; + if (ok) ok = gravity_compiler_emit_c(compiler, staged, prefix, &delegate); + if (ok) { + if (fflush(staged) || fseek(staged, 0, SEEK_SET)) ok = false; + FILE *file = ok ? fopen(output, "wb") : NULL; + if (!file) { fprintf(stderr, "Cannot write AOT output: %s\n", output); ok = false; } + else { + char buffer[4096]; size_t count; + while ((count = fread(buffer, 1, sizeof(buffer), staged))) + if (fwrite(buffer, 1, count, file) != count) { ok = false; break; } + if (ferror(staged)) ok = false; + if (fclose(file)) ok = false; + } + } + if (staged) fclose(staged); + if (compiler) gravity_compiler_free(compiler); + mem_free(source); + return ok ? 0 : 1; +} + int main (int argc, const char* argv[]) { + for (int i = 1; i < argc; ++i) + if (!strcmp(argv[i], "--emit-c")) return gravity_emit_c(argc, argv); // parse arguments and return operation type op_type type = parse_args(argc, argv); diff --git a/src/compiler/gravity_aot.c b/src/compiler/gravity_aot.c new file mode 100644 index 0000000..be3e1a3 --- /dev/null +++ b/src/compiler/gravity_aot.c @@ -0,0 +1,422 @@ +#include "gravity_aot_internal.h" +#include "../shared/gravity_aot_runtime.h" +#include +#include + +bool ga_fail(emitter *e, gnode_t *n, const char *reason) { + if (!e->failed && e->delegate && e->delegate->error_callback) { + error_desc_t loc = ERROR_DESC_NONE; + if (n) loc = (error_desc_t){n->token.lineno, n->token.colno, n->token.fileid, n->token.position}; + char message[256]; + snprintf(message, sizeof(message), "AOT: %s", reason); + e->delegate->error_callback(NULL, GRAVITY_ERROR_SEMANTIC, message, loc, e->delegate->xdata); + } + e->failed = true; return false; +} +void ga_put(emitter *e, const char *format, ...) { + if (e->failed) return; + va_list args; va_start(args, format); + if (vfprintf(e->out, format, args) < 0) ga_fail(e, NULL, "cannot write generated C"); + va_end(args); +} +static bool identifier(const char *s) { + if (!s || !((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z'))) return false; + for (++s; *s; ++s) if (!((*s >= 'a' && *s <= 'z') || (*s >= 'A' && *s <= 'Z') || + (*s >= '0' && *s <= '9') || *s == '_')) return false; + return true; +} +static bool annotation_type(const char *s) { + return !s || identifier(s); +} +static void guard_error(emitter *e) { ga_put(e, "if (ctx->error) goto ga_exit;\n"); } +static gnode_var_t *local(emitter *e, gnode_t *n) { + if (!n || n->tag != NODE_IDENTIFIER_EXPR) { ga_fail(e,n,"expected a local variable"); return NULL; } + gnode_identifier_expr_t *id=(gnode_identifier_expr_t *)n; + if (!id->symbol || id->symbol->tag != NODE_VARIABLE || id->location.type != LOCATION_LOCAL) { + ga_fail(e,n,"expected a local variable; captures are not supported"); return NULL; + } + return (gnode_var_t *)id->symbol; +} +void ga_string(emitter *e, const char *s, size_t length) { + ga_put(e,"\""); + for (size_t i=0; iclasses); ++i) if (gnode_array_get(e->classes,i)==node) return (int)i; + return -1; +} +int ga_declaration_index(emitter *e, gnode_t *node) { + for (size_t i=0; ideclarations); ++i) if (gnode_array_get(e->declarations,i)==node) return (int)i; + return -1; +} +gnode_class_decl_t *ga_function_owner(emitter *e, gnode_function_decl_t *f) { + for (size_t i=0; iclasses); ++i) { + gnode_class_decl_t *c=(gnode_class_decl_t *)gnode_array_get(e->classes,i); + for (size_t j=0; jdecls); ++j) if (gnode_array_get(c->decls,j)==(gnode_t *)f) return c; + } + return NULL; +} +void ga_function_name(emitter *e, gnode_function_decl_t *f) { + /* Use declaration indices to avoid collisions between module and method names. */ + ga_put(e,"%s_fn_%d",e->prefix,ga_declaration_index(e,(gnode_t *)f)); +} +static int self_field(emitter *e, const char *name) { + if (!e->owner) return -1; + unsigned slot=0; + for (size_t i=0; iowner->decls); ++i) { + gnode_t *n=gnode_array_get(e->owner->decls,i); if (n->tag!=NODE_VARIABLE_DECL) continue; + gnode_r *fields=((gnode_variable_decl_t *)n)->decls; + for (size_t j=0; jidentifier,name)) return (int)slot; + } + return -1; +} +static int binary_op(gtoken_t token) { + switch (token) { + case TOK_OP_ADD: return GA_ADD; case TOK_OP_SUB: return GA_SUB; + case TOK_OP_MUL: return GA_MUL; case TOK_OP_DIV: return GA_DIV; + case TOK_OP_REM: return GA_REM; case TOK_OP_LESS: return GA_LT; + case TOK_OP_LESS_EQUAL: return GA_LE; case TOK_OP_GREATER: return GA_GT; + case TOK_OP_GREATER_EQUAL: return GA_GE; case TOK_OP_ISEQUAL: return GA_EQ; + case TOK_OP_ISNOTEQUAL: return GA_NE; case TOK_OP_AND: return GA_AND; + case TOK_OP_OR: return GA_OR; default: return -1; + } +} +static unsigned expr(emitter *e, gnode_t *n); +static void stmt(emitter *e, gnode_t *n); +static void assign(emitter *, gnode_t *, unsigned); +static void postfix(emitter *, gnode_postfix_expr_t *, unsigned, unsigned); +static void emit_args(emitter *, gnode_r *, unsigned); + +static void emit_args(emitter *e, gnode_r *args, unsigned id) { + size_t count=gnode_array_size(args); + ga_put(e,"gravity_aot_value a%u[%zu]; (void)a%u;\n",id,count?count:1,id); + for (size_t i=0; iargnames) return ga_fail(e,(gnode_t *)call,"named arguments are not supported yet"); + emit_args(e,call->args,id); return !e->failed; +} +static bool compiled_function(emitter *e, gnode_t *n) { + for (size_t i=0; ifunctions); ++i) if (gnode_array_get(e->functions,i)==n) return true; + return false; +} +static void postfix(emitter *e, gnode_postfix_expr_t *p, unsigned result, unsigned rhs) { + if (p->is_await) { ga_fail(e,(gnode_t *)p,"await is not supported by AOT yet"); return; } + size_t count=gnode_array_size(p->list), i=0; + if (!count) { ga_fail(e,(gnode_t *)p,"unsupported postfix expression"); return; } + unsigned current=0; + gnode_identifier_expr_t *id=p->id && p->id->tag==NODE_IDENTIFIER_EXPR?(gnode_identifier_expr_t *)p->id:NULL; + gnode_postfix_subexpr_t *first=(gnode_postfix_subexpr_t *)gnode_array_get(p->list,0); + if (id && first->base.tag==NODE_CALL_EXPR) { + unsigned call=++e->temporary; + if (!call_args(e,first,call)) return; + size_t argc=gnode_array_size(first->args); + current=++e->temporary; + ga_put(e,"gravity_aot_value t%u=",current); + int ci=ga_class_index(e,id->symbol); + if (ci>=0) ga_put(e,"gravity_aot_construct(ctx,&ga_%s_types[%d],a%u,%zu)",e->prefix,ci,call,argc); + else if (compiled_function(e,id->symbol)) { + gnode_function_decl_t *f=(gnode_function_decl_t *)id->symbol; + if (argc+1!=gnode_array_size(f->params)) { ga_fail(e,(gnode_t *)p,"call argument count differs from declaration"); return; } + gnode_class_decl_t *owner=ga_function_owner(e,f); + if (owner && owner!=e->owner) { ga_fail(e,(gnode_t *)p,"unbound method call is not supported"); return; } + ga_function_name(e,f); ga_put(e,"(ctx,%sa%u,%zu)",owner?"v0, ":"",call,argc); + } else if (id->symbol && id->symbol->tag==NODE_VARIABLE && id->location.type==LOCATION_GLOBAL) { + ga_put(e,"gravity_aot_call(ctx,gravity_aot_null(),"); ga_string(e,id->value,strlen(id->value)); ga_put(e,",a%u,%zu)",call,argc); + } else { ga_fail(e,(gnode_t *)p,"indirect function calls are not supported yet"); return; } + ga_put(e,";\n"); guard_error(e); i=1; + } else current=expr(e,p->id); + for (; ifailed; ++i) { + gnode_postfix_subexpr_t *step=(gnode_postfix_subexpr_t *)gnode_array_get(p->list,i); + if (step->base.tag==NODE_ACCESS_EXPR) { + if (!step->expr || step->expr->tag!=NODE_IDENTIFIER_EXPR) { ga_fail(e,(gnode_t *)step,"invalid member access"); return; } + const char *key=((gnode_identifier_expr_t *)step->expr)->value; + gnode_postfix_subexpr_t *next=i+1list,i+1):NULL; + if (rhs && i+1==count) { + ga_put(e,"(void)gravity_aot_set(ctx,t%u,",current); ga_string(e,key,strlen(key)); ga_put(e,",t%u);\n",rhs); guard_error(e); return; + } + unsigned value=++e->temporary; + if (next && next->base.tag==NODE_CALL_EXPR) { + unsigned args=++e->temporary; + if (!call_args(e,next,args)) return; + ga_put(e,"gravity_aot_value t%u=gravity_aot_call(ctx,t%u,",value,current); ga_string(e,key,strlen(key)); + ga_put(e,",a%u,%zu);\n",args,gnode_array_size(next->args)); ++i; + } else { + ga_put(e,"gravity_aot_value t%u=gravity_aot_get(ctx,t%u,",value,current); ga_string(e,key,strlen(key)); ga_put(e,");\n"); + } + guard_error(e); current=value; + } else if (step->base.tag==NODE_SUBSCRIPT_EXPR) { + unsigned index=expr(e,step->expr); + if (rhs && i+1==count) { + ga_put(e,"gravity_aot_index_set(ctx,t%u,t%u,t%u);\n",current,index,rhs); guard_error(e); return; + } + unsigned value=++e->temporary; + ga_put(e,"gravity_aot_value t%u=gravity_aot_index_get(ctx,t%u,t%u);\n",value,current,index); guard_error(e); current=value; + } else { ga_fail(e,(gnode_t *)step,"first-class callable values are not supported yet"); return; } + } + if (rhs) ga_fail(e,(gnode_t *)p,"invalid assignment target"); + else ga_put(e,"gravity_aot_value t%u=t%u;\n",result,current); +} +static void assign(emitter *e, gnode_t *target, unsigned rhs) { + if (target && target->tag==NODE_IDENTIFIER_EXPR) { + gnode_identifier_expr_t *id=(gnode_identifier_expr_t *)target; + if (id->location.type==LOCATION_CLASS_IVAR_SAME && e->owner) { + int slot=self_field(e,id->value); + if (slot<0) { ga_fail(e,target,"unknown self field"); return; } + ga_put(e,"(void)gravity_aot_field_set(ctx,v0,%d,t%u);\n",slot,rhs); guard_error(e); + } else { + gnode_var_t *v=local(e,target); + if (v) { ga_put(e,"v%u=gravity_aot_copy(ctx,t%u);\n",v->index,rhs); guard_error(e); } + } + } else if (target && target->tag==NODE_POSTFIX_EXPR) postfix(e,(gnode_postfix_expr_t *)target,++e->temporary,rhs); + else ga_fail(e,target,"unsupported assignment target"); +} + +static unsigned expr(emitter *e, gnode_t *n) { + unsigned t = ++e->temporary; + if (e->failed) return t; + if (!n) { ga_put(e, "gravity_aot_value t%u = gravity_aot_null();\n", t); return t; } + switch (n->tag) { + case NODE_LITERAL_EXPR: { + gnode_literal_expr_t *v = (gnode_literal_expr_t *)n; + if (v->type == LITERAL_INT) + ga_put(e, "gravity_aot_value t%u = gravity_aot_int((int64_t)UINT64_C(%" PRIu64 "));\n", t, (uint64_t)v->value.n64); + else if (v->type == LITERAL_BOOL) + ga_put(e, "gravity_aot_value t%u = gravity_aot_bool(%d);\n", t, !!v->value.n64); + else if (v->type == LITERAL_FLOAT) { + if (!gravity_aot_finite(v->value.d)) { ga_fail(e,n,"non-finite numeric literals are not supported"); break; } + ga_put(e,"gravity_aot_value t%u = gravity_aot_float(%a);\n",t,v->value.d); + } + else if (v->type == LITERAL_STRING) { + ga_put(e,"gravity_aot_value t%u = gravity_aot_string(",t); + ga_string(e,v->value.str,v->len); ga_put(e,", %u);\n",v->len); + } else ga_fail(e,n,"interpolated strings are not supported yet"); + break; + } + case NODE_KEYWORD_EXPR: + if (n->token.type == TOK_KEY_NULL) ga_put(e, "gravity_aot_value t%u = gravity_aot_null();\n", t); + else if (n->token.type == TOK_KEY_TRUE || n->token.type == TOK_KEY_FALSE) + ga_put(e, "gravity_aot_value t%u = gravity_aot_bool(%d);\n", t, n->token.type == TOK_KEY_TRUE); + else ga_fail(e, n, "unsupported keyword expression"); + break; + case NODE_IDENTIFIER_EXPR: { + gnode_identifier_expr_t *id=(gnode_identifier_expr_t *)n; + if (id->location.type == LOCATION_CLASS_IVAR_SAME && e->owner && id->symbol && id->symbol->tag==NODE_VARIABLE) { + int slot=self_field(e,id->value); + if (slot<0) { ga_fail(e,n,"unknown self field"); break; } + ga_put(e,"gravity_aot_value t%u = gravity_aot_field_get(ctx,v0,%d);\n",t,slot); guard_error(e); + } else if (id->location.type == LOCATION_GLOBAL && id->symbol && id->symbol->tag==NODE_VARIABLE && + ((gnode_var_t *)id->symbol)->vdecl && ((gnode_var_t *)id->symbol)->vdecl->storage==TOK_KEY_EXTERN) { + ga_put(e,"gravity_aot_value t%u = gravity_aot_get(ctx,gravity_aot_null(),",t); ga_string(e,id->value,strlen(id->value)); ga_put(e,");\n"); guard_error(e); + } else { + gnode_var_t *v=local(e,n); if (v) ga_put(e,"gravity_aot_value t%u = v%u;\n",t,v->index); + } + break; + } + case NODE_UNARY_EXPR: { + gnode_unary_expr_t *u = (gnode_unary_expr_t *)n; + if (u->op != TOK_OP_ADD && u->op != TOK_OP_SUB && u->op != TOK_OP_NOT) { + ga_fail(e, n, "unsupported unary operator"); break; + } + unsigned v = expr(e, u->expr); + if (u->op == TOK_OP_ADD) ga_put(e, "gravity_aot_value t%u = t%u;\n", t, v); + else ga_put(e, "gravity_aot_value t%u = gravity_aot_unary(ctx, %d, t%u);\n", t, u->op == TOK_OP_SUB ? GA_NEG : GA_NOT, v); + guard_error(e); break; + } + case NODE_BINARY_EXPR: { + gnode_binary_expr_t *b = (gnode_binary_expr_t *)n; + if (b->op == TOK_OP_ASSIGN) { + /* Gravity evaluates the RHS before resolving the assignment target. */ + unsigned right=expr(e,b->right); + assign(e,b->left,right); + ga_put(e,"gravity_aot_value t%u = t%u;\n",t,right); break; + } + if (b->op == TOK_OP_RANGE_INCLUDED || b->op == TOK_OP_RANGE_EXCLUDED) { + unsigned left=expr(e,b->left), right=expr(e,b->right); + ga_put(e,"gravity_aot_value t%u = gravity_aot_range_new(ctx,t%u,t%u,%u);\n",t,left,right,b->op==TOK_OP_RANGE_INCLUDED); + guard_error(e); break; + } + int op = binary_op(b->op); + if (op < 0) { ga_fail(e, n, "unsupported binary operator"); break; } + /* Materialize operands in source order; C call argument order is unspecified. */ + unsigned left = expr(e, b->left), right = expr(e, b->right); + ga_put(e, "gravity_aot_value t%u = gravity_aot_binary(ctx, %d, t%u, t%u);\n", t, op, left, right); + guard_error(e); break; + } + case NODE_POSTFIX_EXPR: postfix(e,(gnode_postfix_expr_t *)n,t,0); break; + case NODE_LIST_EXPR: { + gnode_list_expr_t *l=(gnode_list_expr_t *)n; + if (l->ismap) { ga_fail(e,n,"maps are not supported yet"); break; } + emit_args(e,l->list1,t); + ga_put(e,"gravity_aot_value t%u = gravity_aot_list_new(ctx,a%u,%zu);\n",t,t,gnode_array_size(l->list1)); guard_error(e); break; + } + default: ga_fail(e, n, "unsupported expression (closures and maps require a later backend stage)"); + } + ga_put(e, "(void)t%u;\n", t); + return t; +} + +static void stmt(emitter *e, gnode_t *n) { + if (!n || e->failed) return; + switch (n->tag) { + case NODE_COMPOUND_STAT: case NODE_LIST_STAT: { + gnode_compound_stmt_t *b = (gnode_compound_stmt_t *)n; + ga_put(e, "{\n"); + for (size_t i = 0; i < gnode_array_size(b->stmts); ++i) stmt(e, gnode_array_get(b->stmts, i)); + ga_put(e, "}\n"); break; + } + case NODE_VARIABLE_DECL: { + gnode_variable_decl_t *d = (gnode_variable_decl_t *)n; + if (gnode_array_size(d->base.annotations)) { ga_fail(e,n,"local declaration attributes are not supported yet"); break; } + for (size_t i = 0; i < gnode_array_size(d->decls); ++i) { + gnode_var_t *v = (gnode_var_t *)gnode_array_get(d->decls, i); + if (!annotation_type(v->annotation_type) || v->iscomputed || v->upvalue) { + ga_fail(e, (gnode_t *)v, "computed properties and captured locals are not supported yet"); break; + } + unsigned value = expr(e, v->expr); + ga_put(e, "gravity_aot_value v%u = gravity_aot_copy(ctx,t%u); (void)v%u;\n", v->index, value, v->index); guard_error(e); + } + break; + } + case NODE_FLOW_STAT: { + gnode_flow_stmt_t *f = (gnode_flow_stmt_t *)n; + if (n->token.type != TOK_KEY_IF) { ga_fail(e, n, "only if/else flow is supported yet"); break; } + unsigned c = expr(e, f->cond); + ga_put(e, "if (gravity_aot_truth(ctx, t%u)) {\n", c); stmt(e, f->stmt); ga_put(e, "}\n"); + if (f->elsestmt) { ga_put(e, "else {\n"); stmt(e, f->elsestmt); ga_put(e, "}\n"); } + guard_error(e); break; + } + case NODE_LOOP_STAT: { + gnode_loop_stmt_t *l = (gnode_loop_stmt_t *)n; + if (n->token.type == TOK_KEY_FOR) { + unsigned iterable=expr(e,l->expr), seq=++e->temporary; + if (!l->cond || l->cond->tag != NODE_VARIABLE_DECL || gnode_array_size(((gnode_variable_decl_t *)l->cond)->decls)!=1) { + ga_fail(e,n,"for requires a single declared loop variable"); break; + } + gnode_var_t *v=(gnode_var_t *)gnode_array_get(((gnode_variable_decl_t *)l->cond)->decls,0); + ga_put(e,"{ uint64_t state%u=0; gravity_aot_value v%u=gravity_aot_null(); (void)v%u;\nwhile (1) {\n",seq,v->index,v->index); + ga_put(e,"if (!gravity_aot_tick(ctx)) goto ga_exit;\nint next%u=gravity_aot_next(ctx,t%u,&state%u,&v%u);\n",seq,iterable,seq,v->index); + guard_error(e); ga_put(e,"if (!next%u) break;\n",seq); + ga_put(e,"v%u=gravity_aot_copy(ctx,v%u);\n",v->index,v->index); guard_error(e); + ++e->loop_depth; stmt(e,l->stmt); --e->loop_depth; ga_put(e,"} }\n"); break; + } + if (n->token.type != TOK_KEY_WHILE) { ga_fail(e,n,"repeat loops are not supported yet"); break; } + ga_put(e, "while (1) {\nif (!gravity_aot_tick(ctx)) goto ga_exit;\n"); + unsigned c = expr(e, l->cond); + ga_put(e, "if (!gravity_aot_truth(ctx, t%u)) break;\n", c); guard_error(e); + ++e->loop_depth; stmt(e, l->stmt); --e->loop_depth; + ga_put(e, "}\n"); break; + } + case NODE_JUMP_STAT: { + gnode_jump_stmt_t *j = (gnode_jump_stmt_t *)n; + if (n->token.type == TOK_KEY_RETURN) { + unsigned v = expr(e, j->expr); ga_put(e, "result = t%u; goto ga_exit;\n", v); + } else if (e->loop_depth && (n->token.type == TOK_KEY_BREAK || n->token.type == TOK_KEY_CONTINUE)) + ga_put(e, "%s;\n", n->token.type == TOK_KEY_BREAK ? "break" : "continue"); + else ga_fail(e, n, "unsupported jump statement"); + break; + } + case NODE_EMPTY_STAT: break; + default: (void)expr(e, n); break; + } +} + +static bool add_function(emitter *e, gnode_function_decl_t *f) { + if (!identifier(f->identifier) || !f->block || f->is_async || f->is_closure || f->has_defaults || + gnode_array_size(f->uplist) || f->storage==TOK_KEY_EXTERN || f->storage==TOK_KEY_STATIC) + return ga_fail(e,(gnode_t *)f,"async, static/extern methods, captures and default arguments are not supported yet"); + gnode_array_push(e->functions,(gnode_t *)f); gnode_array_push(e->declarations,(gnode_t *)f); return true; +} +static bool collect(emitter *e, gnode_t *ast) { + if (!ast || ast->tag!=NODE_LIST_STAT) return ga_fail(e,ast,"expected a module AST"); + gnode_r *nodes=((gnode_list_stmt_t *)ast)->stmts; + for (size_t i=0; itag==NODE_EMPTY_STAT) continue; + if (n->tag==NODE_VARIABLE_DECL && ((gnode_variable_decl_t *)n)->storage==TOK_KEY_EXTERN) continue; + if (n->tag==NODE_FUNCTION_DECL) { if (!add_function(e,(gnode_function_decl_t *)n)) return false; continue; } + if (n->tag!=NODE_CLASS_DECL) return ga_fail(e,n,"only functions, classes/structs and extern bindings are supported at module scope"); + gnode_class_decl_t *c=(gnode_class_decl_t *)n; + if (c->storage==TOK_KEY_EXTERN) continue; + if (!identifier(c->identifier) || c->superclass || c->storage==TOK_KEY_STATIC || gnode_array_size(c->protocols)) + return ga_fail(e,n,"inheritance, static classes and protocols are not supported yet"); + gnode_array_push(e->classes,n); gnode_array_push(e->declarations,n); + for (size_t j=0; jdecls); ++j) { + gnode_t *member=gnode_array_get(c->decls,j); + if (member->tag==NODE_FUNCTION_DECL) { if (!add_function(e,(gnode_function_decl_t *)member)) return false; } + else if (member->tag==NODE_VARIABLE_DECL) { + gnode_variable_decl_t *d=(gnode_variable_decl_t *)member; + if (d->storage==TOK_KEY_STATIC || d->storage==TOK_KEY_EXTERN) return ga_fail(e,member,"static/extern fields are not supported yet"); + for (size_t k=0; kdecls); ++k) { + gnode_var_t *v=(gnode_var_t *)gnode_array_get(d->decls,k); + if (v->iscomputed || v->upvalue) return ga_fail(e,(gnode_t *)v,"computed/captured fields are not supported yet"); + gnode_array_push(e->declarations,(gnode_t *)v); + } + } else return ga_fail(e,member,"nested classes and enums are not supported yet"); + } + } + if (!gnode_array_size(e->functions) && !gnode_array_size(e->classes)) return ga_fail(e,ast,"module has no declarations"); + return true; +} +static bool generate(emitter *e, gnode_t *ast) { + if (!identifier(e->prefix)) return ga_fail(e,NULL,"module prefix must be an ASCII identifier starting with a letter"); + if (!collect(e,ast)) return false; + ga_put(e,"/* Generated by Gravity AOT; standalone ABI 2. */\n#include \"gravity_aot_runtime.h\"\n"); + if (gnode_array_size(e->classes)) ga_put(e,"static const gravity_aot_class ga_%s_types[%zu];\n",e->prefix,gnode_array_size(e->classes)); + for (size_t i=0; ifunctions); ++i) { + gnode_function_decl_t *f=(gnode_function_decl_t *)gnode_array_get(e->functions,i); + ga_put(e,"static gravity_aot_value "); ga_function_name(e,f); + ga_put(e,"(gravity_aot_context *, %sconst gravity_aot_value *, uint32_t);\n",ga_function_owner(e,f)?"gravity_aot_value, ":""); + } + for (size_t i=0; ifunctions); ++i) { + gnode_function_decl_t *f=(gnode_function_decl_t *)gnode_array_get(e->functions,i); + e->function=f; e->owner=ga_function_owner(e,f); e->temporary=0; + size_t arity=gnode_array_size(f->params)-1; + ga_put(e,"static gravity_aot_value "); ga_function_name(e,f); + ga_put(e,"(gravity_aot_context *ctx, %sconst gravity_aot_value *args, uint32_t argc) {\n",e->owner?"gravity_aot_value receiver, ":""); + ga_put(e,"gravity_aot_value result=gravity_aot_null(); if (!ctx) return result;\n"); + ga_put(e,"if (ctx->abi_version!=GRAVITY_AOT_ABI_VERSION) { gravity_aot_error(ctx,GRAVITY_AOT_BAD_ABI); return result; }\n"); + ga_put(e,"if (argc!=%zu || (argc && !args)) { gravity_aot_error(ctx,GRAVITY_AOT_ARITY); return result; }\n",arity); + ga_put(e,"if (ctx->depth>=ctx->max_depth || !gravity_aot_tick(ctx)) { gravity_aot_error(ctx,GRAVITY_AOT_LIMIT); return result; }\n++ctx->depth;\n"); + if (e->owner) { + ga_put(e,"gravity_aot_value v0=receiver; (void)v0;\nif (receiver.kind!=GRAVITY_AOT_OBJECT || !receiver.object || receiver.object->type!=&ga_%s_types[%d]) { gravity_aot_error(ctx,GRAVITY_AOT_TYPE); goto ga_exit; }\n",e->prefix,ga_class_index(e,(gnode_t *)e->owner)); + } + for (size_t p=1; pparams); ++p) { + gnode_var_t *v=(gnode_var_t *)gnode_array_get(f->params,p); + ga_put(e,"gravity_aot_value v%u=gravity_aot_copy(ctx,args[%zu]); (void)v%u;\n",v->index,p-1,v->index); guard_error(e); + if (v->annotation_type) { + ga_put(e,"if (!gravity_aot_matches(ctx,v%u,",v->index); ga_string(e,v->annotation_type,strlen(v->annotation_type)); + ga_put(e,")) goto ga_exit;\n"); + } + } + stmt(e,(gnode_t *)f->block); + ga_put(e,"goto ga_exit;\nga_exit:\n--ctx->depth;\nreturn ctx->error?gravity_aot_null():result;\n}\n"); + } + ga_metadata(e); return !e->failed; +} + +bool gravity_compiler_emit_c(gravity_compiler_t *compiler, FILE *output, const char *prefix, gravity_delegate_t *delegate) { + emitter e = {.delegate = delegate, .prefix = prefix}; + if (!compiler || !output) return ga_fail(&e, NULL, "missing compiler or output stream"); + e.out = tmpfile(); + if (!e.out) return ga_fail(&e, NULL, "cannot create staging stream"); + e.functions=gnode_array_create(); e.classes=gnode_array_create(); e.declarations=gnode_array_create(); + bool ok = generate(&e, gravity_compiler_ast(compiler)); + if (e.functions) gnode_array_free(e.functions); + if (e.classes) gnode_array_free(e.classes); + if (e.declarations) gnode_array_free(e.declarations); + if (ok && fflush(e.out) != 0) ok = ga_fail(&e, NULL, "cannot flush staging stream"); + if (ok) { + rewind(e.out); + char bytes[4096]; size_t count; + while ((count = fread(bytes, 1, sizeof(bytes), e.out))) + if (fwrite(bytes, 1, count, output) != count) { ok = ga_fail(&e, NULL, "cannot write output stream"); break; } + if (ferror(e.out)) ok = ga_fail(&e, NULL, "cannot read staging stream"); + } + fclose(e.out); return ok; +} diff --git a/src/compiler/gravity_aot.h b/src/compiler/gravity_aot.h new file mode 100644 index 0000000..a7b311a --- /dev/null +++ b/src/compiler/gravity_aot.h @@ -0,0 +1,19 @@ +#ifndef GRAVITY_AOT_H +#define GRAVITY_AOT_H +#include "gravity_compiler.h" +#include + +#ifdef __cplusplus +extern "C" { +#endif +/* Generate portable C from a successfully compiled module's checked AST. + * Unsupported constructs report a source-located semantic error. Output is + * staged before writing: a semantic failure leaves the output stream untouched. + * prefix must be an ASCII identifier starting with an alphabetic character. + * The compiler retains ownership of its AST. No VM execution is performed. */ +GRAVITY_API bool gravity_compiler_emit_c(gravity_compiler_t *compiler, FILE *output, + const char *prefix, gravity_delegate_t *delegate); +#ifdef __cplusplus +} +#endif +#endif diff --git a/src/compiler/gravity_aot_internal.h b/src/compiler/gravity_aot_internal.h new file mode 100644 index 0000000..e91b442 --- /dev/null +++ b/src/compiler/gravity_aot_internal.h @@ -0,0 +1,23 @@ +#ifndef GRAVITY_AOT_INTERNAL_H +#define GRAVITY_AOT_INTERNAL_H +#include "gravity_aot.h" +#include "../shared/gravity_aot_runtime.h" +typedef struct { + FILE *out; + gravity_delegate_t *delegate; + const char *prefix; + gnode_r *functions, *classes, *declarations; + gnode_function_decl_t *function; + gnode_class_decl_t *owner; + unsigned temporary, loop_depth, metadata_serial; + bool failed; +} emitter; +bool ga_fail(emitter *, gnode_t *, const char *); +void ga_put(emitter *, const char *, ...); +void ga_string(emitter *, const char *, size_t); +int ga_class_index(emitter *, gnode_t *); +int ga_declaration_index(emitter *, gnode_t *); +void ga_function_name(emitter *, gnode_function_decl_t *); +gnode_class_decl_t *ga_function_owner(emitter *, gnode_function_decl_t *); +void ga_metadata(emitter *); +#endif diff --git a/src/compiler/gravity_aot_metadata.c b/src/compiler/gravity_aot_metadata.c new file mode 100644 index 0000000..9f783a0 --- /dev/null +++ b/src/compiler/gravity_aot_metadata.c @@ -0,0 +1,195 @@ +#include "gravity_aot_internal.h" +#include + +static void text(emitter *e, const char *s) { if (s) ga_string(e,s,strlen(s)); else ga_put(e,"NULL"); } +static unsigned attribute_value(emitter *e, gravity_annotation_value_t *v) { + unsigned id=++e->metadata_serial; + size_t count=v->kind==GRAVITY_ANNOTATION_VALUE_LIST?gnode_array_size(v->value.list):0; + if (count) { + unsigned *ids=mem_alloc(NULL,count*sizeof(unsigned)); + if (!ids) { ga_fail(e,NULL,"cannot allocate attribute staging"); return id; } + for (size_t i=0; ivalue.list,i)); + ga_put(e,"static const gravity_aot_attribute_value *const ga_attr_items_%u[] = {",id); + for (size_t i=0; ikind,count); + switch (v->kind) { + case GRAVITY_ANNOTATION_VALUE_IDENTIFIER: case GRAVITY_ANNOTATION_VALUE_STRING: + ga_put(e,".text="); text(e,v->value.string); break; + case GRAVITY_ANNOTATION_VALUE_INT: ga_put(e,".integer=(int64_t)UINT64_C(%" PRIu64 ")",(uint64_t)v->value.integer); break; + case GRAVITY_ANNOTATION_VALUE_FLOAT: + if (!gravity_aot_finite(v->value.floating)) ga_fail(e,NULL,"non-finite attribute values are not supported"); + else ga_put(e,".floating=%a",v->value.floating); break; + case GRAVITY_ANNOTATION_VALUE_BOOL: ga_put(e,".integer=%u",v->value.boolean); break; + case GRAVITY_ANNOTATION_VALUE_LIST: if (count) ga_put(e,".items=ga_attr_items_%u",id); else ga_put(e,".items=NULL"); break; + case GRAVITY_ANNOTATION_VALUE_NULL: ga_put(e,".text=NULL"); break; + } + ga_put(e,"};\n"); return id; +} +static gravity_annotation_r *attributes(gnode_t *node) { + if (node->tag==NODE_VARIABLE) return ((gnode_var_t *)node)->vdecl->base.annotations; + return node->annotations; +} +static void declaration_attributes(emitter *e, gnode_t *node, size_t decl) { + gravity_annotation_r *attrs=attributes(node); + size_t count=gnode_array_size(attrs); + for (size_t i=0; iarguments); + if (!argc) continue; + unsigned *ids=mem_alloc(NULL,argc*sizeof(unsigned)); + if (!ids) { ga_fail(e,node,"cannot allocate annotation arguments"); return; } + for (size_t j=0; jarguments,j); + ids[j]=attribute_value(e,arg->value); + } + ga_put(e,"static const gravity_aot_attribute_argument ga_args_%zu_%zu[] = {\n",decl,i); + for (size_t j=0; jarguments,j); + ga_put(e,"{"); text(e,arg->label); ga_put(e,",&ga_attr_value_%u},\n",ids[j]); + } + ga_put(e,"};\n"); mem_free(ids); + } + if (node->tag==NODE_FUNCTION_DECL) { + gnode_function_decl_t *f=(gnode_function_decl_t *)node; + if (gnode_array_size(f->params)>1) { + ga_put(e,"static const gravity_aot_parameter ga_params_%zu[] = {\n",decl); + for (size_t p=1; pparams); ++p) { + gnode_var_t *v=(gnode_var_t *)gnode_array_get(f->params,p); + ga_put(e,"{"); text(e,v->identifier); ga_put(e,","); text(e,v->annotation_type); ga_put(e,"},\n"); + } + ga_put(e,"};\n"); + } + } + if (!count) return; + ga_put(e,"static const gravity_aot_attribute ga_attrs_%zu[] = {\n",decl); + for (size_t i=0; iarguments); + ga_put(e,"{"); text(e,a->identifier); + ga_put(e,",%zu,%u,%u,%u,",argc,a->token.fileid,a->token.lineno,a->token.colno); + if (argc) ga_put(e,"ga_args_%zu_%zu",decl,i); else ga_put(e,"NULL"); + ga_put(e,"},\n"); + } + ga_put(e,"};\n"); +} +static gnode_class_decl_t *field_owner(emitter *e, gnode_t *v) { + for (size_t i=0; iclasses); ++i) { + gnode_class_decl_t *c=(gnode_class_decl_t *)gnode_array_get(e->classes,i); + for (size_t j=0; jdecls); ++j) { + gnode_t *n=gnode_array_get(c->decls,j); if (n->tag!=NODE_VARIABLE_DECL) continue; + gnode_r *vars=((gnode_variable_decl_t *)n)->decls; + for (size_t k=0; ktag==NODE_KEYWORD_EXPR && n->token.type==TOK_KEY_NULL)) { ga_put(e,"{0}"); return true; } + if (n->tag==NODE_UNARY_EXPR) { + gnode_unary_expr_t *u=(gnode_unary_expr_t *)n; + if (u->op==TOK_OP_SUB || u->op==TOK_OP_ADD) return default_value(e,u->expr,u->op==TOK_OP_SUB?!negative:negative); + } + if (n->tag==NODE_KEYWORD_EXPR && (n->token.type==TOK_KEY_TRUE || n->token.type==TOK_KEY_FALSE)) { + if (negative) ga_put(e,"{.integer=%d,.kind=GRAVITY_AOT_INT}",n->token.type==TOK_KEY_TRUE?-1:0); + else ga_put(e,"{.integer=%u,.kind=GRAVITY_AOT_BOOL}",n->token.type==TOK_KEY_TRUE); return true; + } + if (n->tag==NODE_LITERAL_EXPR) { + gnode_literal_expr_t *v=(gnode_literal_expr_t *)n; + if (v->type==LITERAL_INT || v->type==LITERAL_BOOL) { + uint64_t number=(uint64_t)v->value.n64; if (negative) number=UINT64_C(0)-number; + ga_put(e,"{.integer=(int64_t)UINT64_C(%" PRIu64 "),.kind=%u}",number,v->type==LITERAL_BOOL&&!negative?GRAVITY_AOT_BOOL:GRAVITY_AOT_INT); return true; + } + if (v->type==LITERAL_FLOAT) { if (!gravity_aot_finite(v->value.d)) return ga_fail(e,n,"non-finite field defaults are not supported"); ga_put(e,"{.floating=%a,.kind=GRAVITY_AOT_FLOAT}",negative?-v->value.d:v->value.d); return true; } + if (v->type==LITERAL_STRING && !negative) { + ga_put(e,"{.string="); ga_string(e,v->value.str,v->len); ga_put(e,",.kind=GRAVITY_AOT_STRING,.length=%u}",v->len); return true; + } + } + return ga_fail(e,n,"stored field defaults must be scalar/string constants or null"); +} +void ga_metadata(emitter *e) { + size_t declarations=gnode_array_size(e->declarations), classes=gnode_array_size(e->classes), exports=0; + for (size_t i=0; ideclarations,i),i); + ga_put(e,"static const gravity_aot_declaration ga_%s_declarations[] = {\n",e->prefix); + for (size_t i=0; ideclarations,i); + gnode_class_decl_t *parent=n->tag==NODE_VARIABLE?field_owner(e,n):n->tag==NODE_FUNCTION_DECL?ga_function_owner(e,(gnode_function_decl_t *)n):NULL; + unsigned kind=n->tag==NODE_VARIABLE?GRAVITY_AOT_DECL_FIELD:n->tag==NODE_CLASS_DECL? + (((gnode_class_decl_t *)n)->is_struct?GRAVITY_AOT_DECL_STRUCT:GRAVITY_AOT_DECL_CLASS): + parent?GRAVITY_AOT_DECL_METHOD:GRAVITY_AOT_DECL_FUNCTION; + ga_put(e,"{"); text(e,gnode_identifier(n)); ga_put(e,","); text(e,parent?parent->identifier:NULL); ga_put(e,","); + text(e,n->tag==NODE_VARIABLE?((gnode_var_t *)n)->annotation_type:NULL); + size_t count=gnode_array_size(attributes(n)); + ga_put(e,",%u,%u,%u,%u,%zu,",kind,n->token.fileid,n->token.lineno,n->token.colno,count); + if (count) ga_put(e,"ga_attrs_%zu",i); else ga_put(e,"NULL"); + size_t params=n->tag==NODE_FUNCTION_DECL?gnode_array_size(((gnode_function_decl_t *)n)->params)-1:0; + ga_put(e,",%zu,",params); + if (params) ga_put(e,"ga_params_%zu",i); else ga_put(e,"NULL"); ga_put(e,"},\n"); + } + ga_put(e,"};\n"); + for (size_t i=0; iclasses,i); + size_t fields=0, methods=0; + for (size_t j=0; jdecls); ++j) { + gnode_t *n=gnode_array_get(c->decls,j); + if (n->tag==NODE_VARIABLE_DECL) fields+=gnode_array_size(((gnode_variable_decl_t *)n)->decls); + if (n->tag==NODE_FUNCTION_DECL) ++methods; + } + if (fields) { + ga_put(e,"static const gravity_aot_field ga_fields_%zu[] = {\n",i); + for (size_t j=0; jdecls); ++j) { + gnode_t *n=gnode_array_get(c->decls,j); if (n->tag!=NODE_VARIABLE_DECL) continue; + gnode_variable_decl_t *d=(gnode_variable_decl_t *)n; + for (size_t k=0; kdecls); ++k) { + gnode_var_t *v=(gnode_var_t *)gnode_array_get(d->decls,k); + ga_put(e,"{&ga_%s_declarations[%d],",e->prefix,ga_declaration_index(e,(gnode_t *)v)); + default_value(e,v->expr,false); ga_put(e,",%u},\n",d->type==TOK_KEY_CONST); + } + } + ga_put(e,"};\n"); + } + if (methods) { + ga_put(e,"static const gravity_aot_method_export ga_methods_%zu[] = {\n",i); + for (size_t j=0; jdecls); ++j) { + gnode_t *n=gnode_array_get(c->decls,j); if (n->tag!=NODE_FUNCTION_DECL) continue; + gnode_function_decl_t *f=(gnode_function_decl_t *)n; + ga_put(e,"{"); text(e,f->identifier); ga_put(e,",%zu,",gnode_array_size(f->params)-1); ga_function_name(e,f); + ga_put(e,",&ga_%s_declarations[%d]},\n",e->prefix,ga_declaration_index(e,n)); + } + ga_put(e,"};\n"); + } + } + if (classes) { + ga_put(e,"static const gravity_aot_class ga_%s_types[%zu] = {\n",e->prefix,classes); + for (size_t i=0; iclasses,i); + size_t fields=0,methods=0; + for (size_t j=0; jdecls); ++j) { + gnode_t *n=gnode_array_get(c->decls,j); + if (n->tag==NODE_VARIABLE_DECL) fields+=gnode_array_size(((gnode_variable_decl_t *)n)->decls); + if (n->tag==NODE_FUNCTION_DECL) ++methods; + } + ga_put(e,"{&ga_%s_declarations[%d],%u,%zu,%zu,",e->prefix,ga_declaration_index(e,(gnode_t *)c),c->is_struct,fields,methods); + if (fields) ga_put(e,"ga_fields_%zu",i); else ga_put(e,"NULL"); ga_put(e,","); + if (methods) ga_put(e,"ga_methods_%zu",i); else ga_put(e,"NULL"); ga_put(e,"},\n"); + } + ga_put(e,"};\n"); + } + for (size_t i=0; ifunctions); ++i) + if (!ga_function_owner(e,(gnode_function_decl_t *)gnode_array_get(e->functions,i))) ++exports; + if (exports) { + ga_put(e,"static const gravity_aot_export ga_%s_exports[] = {\n",e->prefix); + for (size_t i=0; ifunctions); ++i) { + gnode_function_decl_t *f=(gnode_function_decl_t *)gnode_array_get(e->functions,i); if (ga_function_owner(e,f)) continue; + ga_put(e,"{"); text(e,f->identifier); ga_put(e,",%zu,",gnode_array_size(f->params)-1); ga_function_name(e,f); + ga_put(e,",&ga_%s_declarations[%d]},\n",e->prefix,ga_declaration_index(e,(gnode_t *)f)); + } + ga_put(e,"};\n"); + } + ga_put(e,"const gravity_aot_module *%s_get_module(void) {\nstatic const gravity_aot_module m={GRAVITY_AOT_ABI_VERSION,%zu,",e->prefix,exports); + if (exports) ga_put(e,"ga_%s_exports",e->prefix); else ga_put(e,"NULL"); + ga_put(e,",%zu,%zu,",classes,declarations); + if (classes) ga_put(e,"ga_%s_types",e->prefix); else ga_put(e,"NULL"); + ga_put(e,",ga_%s_declarations};\nreturn &m;\n}\n",e->prefix); +} diff --git a/src/compiler/gravity_parser.c b/src/compiler/gravity_parser.c index 376af90..7f2f7a5 100644 --- a/src/compiler/gravity_parser.c +++ b/src/compiler/gravity_parser.c @@ -404,7 +404,14 @@ static gnode_t *parse_function (gravity_parser_t *parser, bool is_declaration, g const char *identifier = NULL; if (is_declaration) { gtoken_t peek = gravity_lexer_peek(lexer); - identifier = (token_isoperator(peek)) ? string_dup(token_name(gravity_lexer_next(lexer))) : parse_identifier(parser); + // AdaScript lifecycle method `event` shares a token with the unsupported + // event-declaration syntax. Permit it only as a class/struct method name. + if (peek == TOK_KEY_EVENT && IS_CLASS_ENCLOSED() && !IS_FUNCTION_ENCLOSED()) { + gravity_lexer_next(lexer); + identifier = string_dup("event"); + } else { + identifier = (token_isoperator(peek)) ? string_dup(token_name(gravity_lexer_next(lexer))) : parse_identifier(parser); + } DEBUG_PARSER("parse_function_declaration %s", identifier); } @@ -2753,6 +2760,23 @@ static gravity_annotation_value_t *parse_annotation_value(gravity_parser_t *pars DECLARE_LEXER; gtoken_t peek = gravity_lexer_peek(lexer); + if (peek == TOK_OP_SUB || peek == TOK_OP_ADD) { + gravity_lexer_next(lexer); + gtoken_s sign = gravity_lexer_token(lexer); + if (gravity_lexer_peek(lexer) != TOK_NUMBER) { + REPORT_ERROR(sign, "%s", "An annotation sign must precede a numeric literal."); + return NULL; + } + gravity_annotation_value_t *value = parse_annotation_value(parser); + if (value && peek == TOK_OP_SUB) { + if (value->kind == GRAVITY_ANNOTATION_VALUE_INT) + value->value.integer = (int64_t)(UINT64_C(0) - (uint64_t)value->value.integer); + else if (value->kind == GRAVITY_ANNOTATION_VALUE_FLOAT) + value->value.floating = -value->value.floating; + } + return value; + } + if (peek == TOK_OP_OPEN_SQUAREBRACKET) { gravity_lexer_next(lexer); gravity_annotation_value_t *value = gravity_annotation_value_create(GRAVITY_ANNOTATION_VALUE_LIST); diff --git a/src/shared/gravity_aot_objects.h b/src/shared/gravity_aot_objects.h new file mode 100644 index 0000000..f22f379 --- /dev/null +++ b/src/shared/gravity_aot_objects.h @@ -0,0 +1,179 @@ +#ifndef GRAVITY_AOT_OBJECTS_H +#define GRAVITY_AOT_OBJECTS_H +#include "gravity_aot_runtime.h" +/* The caller supplies memory. No implicit GC, global allocator, or shared VM. */ +typedef struct { unsigned char *buffer; size_t capacity, used; } gravity_aot_arena; +static inline void *gravity_aot_arena_allocate(void *data, size_t size) { + gravity_aot_arena *a = (gravity_aot_arena *)data; + if (!a || !a->buffer || a->used > a->capacity) return NULL; + size_t alignment = sizeof(double) > sizeof(void *) ? sizeof(double) : sizeof(void *); + size_t remainder = ((uintptr_t)a->buffer + a->used) % alignment; + size_t padding = remainder ? alignment - remainder : 0; + if (padding > a->capacity-a->used || size > a->capacity-a->used-padding) return NULL; + void *p = a->buffer+a->used+padding; a->used += padding+size; return p; +} +static inline void *gravity_aot_allocate(gravity_aot_context *c, size_t size) { + if (c->error) return NULL; + void *p = c->allocate ? c->allocate(c->allocation_data, size) : NULL; + if (!p) gravity_aot_error(c, GRAVITY_AOT_MEMORY); + return p; +} +static inline int gravity_aot_matches(gravity_aot_context *c, gravity_aot_value v, const char *type) { + if (!gravity_aot_valid(c,v)) return 0; + int match=!gravity_aot_text_compare(type,"Int")?v.kind==GRAVITY_AOT_INT:!gravity_aot_text_compare(type,"Bool")?v.kind==GRAVITY_AOT_BOOL: + !gravity_aot_text_compare(type,"Float")?v.kind==GRAVITY_AOT_FLOAT:!gravity_aot_text_compare(type,"String")?v.kind==GRAVITY_AOT_STRING: + !gravity_aot_text_compare(type,"List")?v.kind==GRAVITY_AOT_LIST: + v.kind==GRAVITY_AOT_HOST || (v.kind==GRAVITY_AOT_OBJECT && v.object && !gravity_aot_text_compare(type,v.object->type->declaration->name)); + if (!match) gravity_aot_error(c,GRAVITY_AOT_TYPE); return match; +} +static inline gravity_aot_value gravity_aot_copy(gravity_aot_context *c, gravity_aot_value v); +static inline gravity_aot_value gravity_aot_new(gravity_aot_context *c, const gravity_aot_class *type) { + gravity_aot_value v = gravity_aot_null(); + if (!type || !gravity_aot_tick(c)) return v; + if ((size_t)type->field_count > (SIZE_MAX-sizeof(gravity_aot_object))/sizeof(gravity_aot_value)) { + gravity_aot_error(c, GRAVITY_AOT_MEMORY); return v; + } + gravity_aot_object *o = (gravity_aot_object *)gravity_aot_allocate(c, sizeof(*o)+type->field_count*sizeof(gravity_aot_value)); + if (!o) return v; + o->type = type; o->fields = (gravity_aot_value *)(o+1); + for (uint32_t i=0; ifield_count; ++i) o->fields[i] = gravity_aot_copy(c, type->fields[i].default_value); + v.object = o; v.kind = GRAVITY_AOT_OBJECT; return v; +} +static inline gravity_aot_value gravity_aot_copy(gravity_aot_context *c, gravity_aot_value v) { + if (!gravity_aot_valid(c, v)) return gravity_aot_null(); + if (v.kind == GRAVITY_AOT_OBJECT && v.object && v.object->type->is_struct) { + gravity_aot_value copy = gravity_aot_new(c, v.object->type); + if (c->error) return gravity_aot_null(); + for (uint32_t i=0; itype->field_count; ++i) copy.object->fields[i] = gravity_aot_copy(c, v.object->fields[i]); + return copy; + } + return v; +} +/* Compiled self-field accesses use a resolved slot, avoiding name lookup. */ +static inline gravity_aot_value gravity_aot_field_get(gravity_aot_context *c, gravity_aot_value receiver, uint32_t slot) { + if (!gravity_aot_valid(c,receiver)) return gravity_aot_null(); + if (receiver.kind!=GRAVITY_AOT_OBJECT || !receiver.object || slot>=receiver.object->type->field_count) { + gravity_aot_error(c,GRAVITY_AOT_FIELD); return gravity_aot_null(); + } + return receiver.object->fields[slot]; +} +static inline int gravity_aot_field_set(gravity_aot_context *c, gravity_aot_value receiver, uint32_t slot, gravity_aot_value value) { + if (!gravity_aot_valid(c,receiver) || !gravity_aot_valid(c,value)) return 0; + if (receiver.kind!=GRAVITY_AOT_OBJECT || !receiver.object || slot>=receiver.object->type->field_count || receiver.object->type->fields[slot].readonly) { + gravity_aot_error(c,GRAVITY_AOT_FIELD); return 0; + } + receiver.object->fields[slot]=gravity_aot_copy(c,value); return !c->error; +} +static inline gravity_aot_value gravity_aot_get(gravity_aot_context *c, gravity_aot_value receiver, const char *name) { + gravity_aot_value result = gravity_aot_null(); + if (!gravity_aot_valid(c, receiver)) return result; + if (receiver.kind == GRAVITY_AOT_OBJECT && receiver.object) { + const gravity_aot_class *t = receiver.object->type; + for (uint32_t i=0; ifield_count; ++i) + if (!gravity_aot_text_compare(name,t->fields[i].declaration->name)) return receiver.object->fields[i]; + gravity_aot_error(c, GRAVITY_AOT_FIELD); + } else if (receiver.kind == GRAVITY_AOT_LIST && !gravity_aot_text_compare(name,"count")) return gravity_aot_int(receiver.list->count); + else if (receiver.kind == GRAVITY_AOT_STRING && !gravity_aot_text_compare(name,"count")) { + gravity_aot_error(c, GRAVITY_AOT_TYPE); /* byte length is not Unicode character count */ + } else if ((receiver.kind == GRAVITY_AOT_HOST || receiver.kind == GRAVITY_AOT_NULL) && c->host && c->host->get) { + if (!c->host->get(c->host_data,receiver,name,&result)) gravity_aot_error(c, GRAVITY_AOT_HOST_ERROR); + gravity_aot_valid(c,result); + } else gravity_aot_error(c, GRAVITY_AOT_FIELD); + return result; +} +static inline int gravity_aot_set(gravity_aot_context *c, gravity_aot_value receiver, const char *name, gravity_aot_value value) { + if (!gravity_aot_valid(c,receiver) || !gravity_aot_valid(c,value)) return 0; + if (receiver.kind == GRAVITY_AOT_OBJECT && receiver.object) { + const gravity_aot_class *t = receiver.object->type; + for (uint32_t i=0; ifield_count; ++i) if (!gravity_aot_text_compare(name,t->fields[i].declaration->name)) { + if (t->fields[i].readonly) { gravity_aot_error(c, GRAVITY_AOT_FIELD); return 0; } + receiver.object->fields[i] = gravity_aot_copy(c,value); return !c->error; + } + gravity_aot_error(c, GRAVITY_AOT_FIELD); + } else if (receiver.kind == GRAVITY_AOT_HOST && c->host && c->host->set) { + if (!c->host->set(c->host_data,receiver,name,value)) gravity_aot_error(c, GRAVITY_AOT_HOST_ERROR); + } else gravity_aot_error(c, GRAVITY_AOT_FIELD); + return !c->error; +} +static inline gravity_aot_value gravity_aot_call(gravity_aot_context *c, gravity_aot_value receiver, + const char *name, const gravity_aot_value *args, uint32_t count) { + gravity_aot_value result = gravity_aot_null(); + if (!gravity_aot_valid(c,receiver)) return result; + if (count && !args) { gravity_aot_error(c,GRAVITY_AOT_ARITY); return result; } + for (uint32_t i=0; itype; + for (uint32_t i=0; imethod_count; ++i) if (!gravity_aot_text_compare(name,t->methods[i].name)) + return t->methods[i].call(c,receiver,args,count); + gravity_aot_error(c, GRAVITY_AOT_METHOD); + } else if ((receiver.kind == GRAVITY_AOT_HOST || receiver.kind == GRAVITY_AOT_NULL) && c->host && c->host->call) { + if (!c->host->call(c->host_data,receiver,name,args,count,&result)) gravity_aot_error(c, GRAVITY_AOT_HOST_ERROR); + gravity_aot_valid(c,result); + } else gravity_aot_error(c, GRAVITY_AOT_HOST_ERROR); + return result; +} +static inline gravity_aot_value gravity_aot_construct(gravity_aot_context *c, const gravity_aot_class *t, + const gravity_aot_value *args, uint32_t count) { + gravity_aot_value v = gravity_aot_new(c,t); + if (c->error) return gravity_aot_null(); + for (uint32_t i=0; imethod_count; ++i) if (!gravity_aot_text_compare(t->methods[i].name,"init")) { + (void)t->methods[i].call(c,v,args,count); return c->error ? gravity_aot_null() : v; + } + if (count) { gravity_aot_error(c, GRAVITY_AOT_ARITY); return gravity_aot_null(); } + return v; +} +static inline gravity_aot_value gravity_aot_list_new(gravity_aot_context *c, const gravity_aot_value *items, uint32_t count) { + gravity_aot_value v = gravity_aot_null(); + if ((size_t)count > (SIZE_MAX-sizeof(gravity_aot_list))/sizeof(gravity_aot_value)) { + gravity_aot_error(c, GRAVITY_AOT_MEMORY); return v; + } + gravity_aot_list *list = (gravity_aot_list *)gravity_aot_allocate(c,sizeof(*list)+count*sizeof(gravity_aot_value)); + if (!list) return v; + list->count=count; list->items=(gravity_aot_value *)(list+1); + for (uint32_t i=0; iitems[i]=gravity_aot_copy(c,items[i]); + v.kind=GRAVITY_AOT_LIST; v.list=list; return v; +} +static inline gravity_aot_value gravity_aot_range_new(gravity_aot_context *c, gravity_aot_value start, gravity_aot_value end, uint32_t inclusive) { + gravity_aot_value v = gravity_aot_null(); + if (start.kind != GRAVITY_AOT_INT || end.kind != GRAVITY_AOT_INT) { gravity_aot_error(c,GRAVITY_AOT_TYPE); return v; } + gravity_aot_range *r=(gravity_aot_range *)gravity_aot_allocate(c,sizeof(*r)); + if (!r) return v; + r->start=start.integer; r->end=end.integer; r->inclusive=inclusive; + v.kind=GRAVITY_AOT_RANGE; v.range=r; return v; +} +static inline int gravity_aot_next(gravity_aot_context *c, gravity_aot_value iterable, uint64_t *state, gravity_aot_value *value) { + if (!gravity_aot_valid(c,iterable)) return -1; + if (iterable.kind == GRAVITY_AOT_LIST) { + if (*state >= iterable.list->count) return 0; + *value=iterable.list->items[(*state)++]; return 1; + } + if (iterable.kind == GRAVITY_AOT_RANGE) { + gravity_aot_range *r=iterable.range; + uint64_t span = r->end >= r->start ? (uint64_t)r->end-(uint64_t)r->start : (uint64_t)r->start-(uint64_t)r->end; + if (span==UINT64_MAX) { gravity_aot_error(c,GRAVITY_AOT_LIMIT); return -1; } + if (*state > span || (*state == span && !r->inclusive)) return 0; + *value=gravity_aot_int((int64_t)(r->end >= r->start ? (uint64_t)r->start+*state : (uint64_t)r->start-*state)); + ++*state; return 1; + } + if (iterable.kind == GRAVITY_AOT_HOST && c->host && c->host->next) { + int next=c->host->next(c->host_data,iterable,state,value); + if (next < 0 || (next > 0 && !gravity_aot_valid(c,*value))) gravity_aot_error(c,GRAVITY_AOT_HOST_ERROR); + return c->error ? -1 : next; + } + gravity_aot_error(c,GRAVITY_AOT_TYPE); return -1; +} +static inline gravity_aot_value gravity_aot_index_get(gravity_aot_context *c, gravity_aot_value v, gravity_aot_value index) { + if (!gravity_aot_valid(c,v)) return gravity_aot_null(); + if (v.kind==GRAVITY_AOT_LIST && index.kind==GRAVITY_AOT_INT && index.integer>=0 && + (uint64_t)index.integercount) return v.list->items[index.integer]; + gravity_aot_error(c,GRAVITY_AOT_FIELD); return gravity_aot_null(); +} +static inline int gravity_aot_index_set(gravity_aot_context *c, gravity_aot_value v, gravity_aot_value index, gravity_aot_value value) { + if (!gravity_aot_valid(c,v)) return 0; + if (v.kind==GRAVITY_AOT_LIST && index.kind==GRAVITY_AOT_INT && index.integer>=0 && + (uint64_t)index.integercount) { + v.list->items[index.integer]=gravity_aot_copy(c,value); return !c->error; + } + gravity_aot_error(c,GRAVITY_AOT_FIELD); return 0; +} +#endif diff --git a/src/shared/gravity_aot_runtime.h b/src/shared/gravity_aot_runtime.h new file mode 100644 index 0000000..abaaf28 --- /dev/null +++ b/src/shared/gravity_aot_runtime.h @@ -0,0 +1,245 @@ +#ifndef GRAVITY_AOT_RUNTIME_H +#define GRAVITY_AOT_RUNTIME_H +/* Experimental standalone ABI. No bytecode VM dependency. + * Context, allocation arena and host handles belong to the caller. */ +#include +#include +/* Keep ordinary object/metadata operations usable in freestanding targets. + * Floating remainder uses the target C math runtime (IEEE remainder, as in VM). */ +#ifdef __cplusplus +extern "C" double remainder(double, double) noexcept; +#else +extern double remainder(double, double); +#endif +static inline int gravity_aot_text_compare(const char *a, const char *b) { + while (*a && *a==*b) { ++a; ++b; } + return (unsigned char)*a-(unsigned char)*b; +} +static inline int gravity_aot_bytes_compare(const char *a, const char *b, size_t count) { + for (size_t i=0; ihost_generation; return v; +} +static inline void gravity_aot_error(gravity_aot_context *c, uint32_t error) { if (!c->error) c->error = error; } +static inline int gravity_aot_valid(gravity_aot_context *c, gravity_aot_value v) { + if (v.kind > GRAVITY_AOT_RANGE) gravity_aot_error(c, GRAVITY_AOT_TYPE); + if (v.kind == GRAVITY_AOT_HOST && v.length != c->host_generation) gravity_aot_error(c, GRAVITY_AOT_STALE_HANDLE); + return !c->error; +} +static inline int gravity_aot_tick(gravity_aot_context *c) { + if (c->error) return 0; + if (!c->fuel) { gravity_aot_error(c, GRAVITY_AOT_LIMIT); return 0; } + --c->fuel; return 1; +} +static inline int gravity_aot_truth(gravity_aot_context *c, gravity_aot_value v) { + if (!gravity_aot_valid(c, v)) return 0; + if (v.kind == GRAVITY_AOT_NULL) return 0; + if (v.kind == GRAVITY_AOT_FLOAT) return v.floating != 0.0; + if (v.kind == GRAVITY_AOT_STRING) return v.length != 0; + if (v.kind == GRAVITY_AOT_INT || v.kind == GRAVITY_AOT_BOOL) return v.integer != 0; + return 1; +} +static inline double gravity_aot_number(gravity_aot_value v) { return v.kind == GRAVITY_AOT_FLOAT ? v.floating : (double)v.integer; } +static inline int64_t gravity_aot_integer(gravity_aot_context *c, gravity_aot_value v) { + if (v.kind != GRAVITY_AOT_FLOAT) return v.integer; + if (!gravity_aot_finite(v.floating) || v.floating >= 9223372036854775808.0 || v.floating < -9223372036854775808.0) { + gravity_aot_error(c, GRAVITY_AOT_TYPE); return 0; + } + return (int64_t)v.floating; +} +static inline gravity_aot_value gravity_aot_binary(gravity_aot_context *c, int op, gravity_aot_value a, gravity_aot_value b) { + if (!gravity_aot_valid(c, a) || !gravity_aot_valid(c, b)) return gravity_aot_null(); + if (op == GA_AND || op == GA_OR) { + int x = gravity_aot_truth(c, a), y = gravity_aot_truth(c, b); + return gravity_aot_bool(op == GA_AND ? x && y : x || y); + } + if (a.kind == GRAVITY_AOT_STRING && b.kind == GRAVITY_AOT_STRING && op >= GA_LT && op <= GA_NE) { + uint32_t n = a.length < b.length ? a.length : b.length; + int cmp = gravity_aot_bytes_compare(a.string, b.string, n); + if (!cmp) cmp = a.length > b.length ? 1 : a.length < b.length ? -1 : 0; + return gravity_aot_bool(op == GA_EQ ? !cmp : op == GA_NE ? !!cmp : op == GA_LT ? cmp < 0 : + op == GA_LE ? cmp <= 0 : op == GA_GT ? cmp > 0 : cmp >= 0); + } + if (a.kind > GRAVITY_AOT_FLOAT || b.kind > GRAVITY_AOT_FLOAT) { + if (op == GA_EQ || op == GA_NE) { + int equal = a.kind == b.kind && a.handle == b.handle; + return gravity_aot_bool(op == GA_EQ ? equal : !equal); + } + gravity_aot_error(c, GRAVITY_AOT_TYPE); return gravity_aot_null(); + } + if ((op == GA_DIV || op == GA_REM) && (b.kind == GRAVITY_AOT_NULL || + (b.kind == GRAVITY_AOT_INT && !b.integer) || (b.kind == GRAVITY_AOT_FLOAT && b.floating == 0.0))) { + gravity_aot_error(c, GRAVITY_AOT_DIV_ZERO); return gravity_aot_null(); + } + if (a.kind == GRAVITY_AOT_NULL && op < GA_LT) { + if (op == GA_ADD) return b; + if (op == GA_MUL || op == GA_DIV || op == GA_REM) return gravity_aot_int(0); + } + int float_math = a.kind == GRAVITY_AOT_FLOAT || + (a.kind == GRAVITY_AOT_INT && b.kind == GRAVITY_AOT_FLOAT && op != GA_REM) || + (op >= GA_LT && b.kind == GRAVITY_AOT_FLOAT); + if (float_math) { + double x = gravity_aot_number(a), y = gravity_aot_number(b); + switch (op) { + case GA_ADD: return gravity_aot_float(x+y); case GA_SUB: return gravity_aot_float(x-y); + case GA_MUL: return gravity_aot_float(x*y); + case GA_DIV: case GA_REM: + if (y == 0.0) { gravity_aot_error(c, GRAVITY_AOT_DIV_ZERO); return gravity_aot_null(); } + return gravity_aot_float(op == GA_DIV ? x/y : remainder(x,y)); + case GA_LT: return gravity_aot_bool(xy); case GA_GE: return gravity_aot_bool(x>=y); + case GA_EQ: return gravity_aot_bool(x==y); case GA_NE: return gravity_aot_bool(x!=y); + } + } + int64_t x = gravity_aot_integer(c, a), y = gravity_aot_integer(c, b); + if (c->error) return gravity_aot_null(); + switch (op) { + case GA_ADD: return gravity_aot_int((int64_t)((uint64_t)x+(uint64_t)y)); + case GA_SUB: return gravity_aot_int((int64_t)((uint64_t)x-(uint64_t)y)); + case GA_MUL: return gravity_aot_int((int64_t)((uint64_t)x*(uint64_t)y)); + case GA_DIV: case GA_REM: + if (!y) { gravity_aot_error(c, GRAVITY_AOT_DIV_ZERO); return gravity_aot_null(); } + if (x == INT64_MIN && y == -1) return gravity_aot_int(op == GA_DIV ? INT64_MIN : 0); + return gravity_aot_int(op == GA_DIV ? x/y : x%y); + case GA_LT: return gravity_aot_bool(xy); case GA_GE: return gravity_aot_bool(x>=y); + case GA_EQ: return gravity_aot_bool(x==y); case GA_NE: return gravity_aot_bool(x!=y); + default: gravity_aot_error(c, GRAVITY_AOT_TYPE); return gravity_aot_null(); + } +} +static inline gravity_aot_value gravity_aot_unary(gravity_aot_context *c, int op, gravity_aot_value v) { + if (op == GA_NOT) return gravity_aot_bool(!gravity_aot_truth(c, v)); + if (!gravity_aot_valid(c, v)) return gravity_aot_null(); + if (v.kind == GRAVITY_AOT_NULL) return gravity_aot_bool(0); + if (v.kind == GRAVITY_AOT_FLOAT) return gravity_aot_float(-v.floating); + if (v.kind > GRAVITY_AOT_BOOL) { gravity_aot_error(c, GRAVITY_AOT_TYPE); return gravity_aot_null(); } + return gravity_aot_int((int64_t)(UINT64_C(0)-(uint64_t)v.integer)); +} +#include "gravity_aot_objects.h" +#endif diff --git a/test/aot/attributes.gravity b/test/aot/attributes.gravity new file mode 100644 index 0000000..9609052 --- /dev/null +++ b/test/aot/attributes.gravity @@ -0,0 +1,68 @@ +@component(id: "aot.position") +struct Position { + @export var x = 1.0; + @export var y = -2.0; +} +@resource(id: "aot.time", autoInsert: true) +struct Time { + @export var scale = 2.0; +} +@replicated_component(id: "aot.health", version: 2, authority: "host", visibility: "all_peers") +struct Health { + @network_field(tag: 1, quantization: "int") var value = 100; + @local var dirty = false; +} +@network_command(id: "aot.hit", version: 1, direction: "to_host", delivery: "reliable", channel: "gameplay") +struct Hit { + @network_field(tag: 1) var damage = 5; +} +@system(id: "aot.movement", scheduler: "update") +@after(id: "input") +@before(id: "render") +class Movement { + @query(Position, with: [Active, Visible], without: Paused) var rows; + @resource(Time) var timing; + var calls = 0; + func update(context) { + calls = calls + 1; + for (var row in rows) { + row.Position.x = row.Position.x + context.deltaTime * timing.scale; + } + context.world.notify(calls); + } + @rpc(id: "aot.damage", direction: "to_host", delivery: "reliable") + func damage(amount: Int) { calls = calls + amount; } +} +@scriptable(id: "aot.controller", version: 2, aliases: ["OldController", "OlderController"]) +class Controller { + @component(Position, required: true) var position; + @resource(Time, optional: true) var clock; + @export var health = 10; + @export var label = "日本"; + func ready(context) { health = health + context.bonus; } + func update(context) { position.x = position.x + context.deltaTime; } + func fixedUpdate(context) { health = health - 1; } + func event(context) { health = health + context.bonus; } + func destroy(context) { health = 0; } +} +@tool(id: "aot.tool", name: "Native Tool", version: "1.0", apiVersion: 1, + permissions: ["workspace.read"], platforms: [macos, ipados]) +class Tool { func activate(editor) { editor.notify(1); } } +class Counter { + var value = 1; + func init(n: Int) { value = n; } + func add(n: Int) { value = value + n; return value; } +} +@metadata(note: "quotes: \" and slash", values: [Position, "text", -3, 2.5, true, null, ["nested"]]) +func main() { + var a = Position(); + var b = a; + b.x = 20.0; + var c = Counter(2); + var d = c; + d.add(3); + var total = 0; + for (var i in 1...3) { total = total + i; } + for (var i in [1, 2, 3]) { total = total + i; } + return c.value + total + a.x; +} diff --git a/test/aot/attributes_host.c b/test/aot/attributes_host.c new file mode 100644 index 0000000..43932db --- /dev/null +++ b/test/aot/attributes_host.c @@ -0,0 +1,161 @@ +#include "attributes.h" +#include +#include +#include + +enum { POSITION=1, QUERY, CONTEXT, WORLD }; +typedef struct { int kind; double x; int active, visible, paused; } position; +typedef struct { int kind; } handle; +typedef struct { + position positions[4]; + handle query, context, world; + gravity_aot_context *ctx; + const gravity_aot_attribute *query_metadata; + int notified; +} host_world; + +static const gravity_aot_class *type(const char *name) { + const gravity_aot_module *m=attributes_get_module(); + assert(m->abi_version==GRAVITY_AOT_ABI_VERSION); + for (uint32_t i=0; itype_count; ++i) if (!strcmp(m->types[i].declaration->name,name)) return &m->types[i]; + assert(!"missing native type"); return NULL; +} +static const gravity_aot_attribute *attribute(const gravity_aot_declaration *d,const char *name) { + for (uint32_t i=0; iattribute_count; ++i) if (!strcmp(d->attributes[i].name,name)) return &d->attributes[i]; + assert(!"missing attribute"); return NULL; +} +static const gravity_aot_attribute_value *arg(const gravity_aot_attribute *a,const char *label) { + for (uint32_t i=0; iargument_count; ++i) { + const gravity_aot_attribute_argument *v=&a->arguments[i]; + if ((!label && !v->label) || (label && v->label && !strcmp(label,v->label))) return v->value; + } + assert(!"missing argument"); return NULL; +} +static const gravity_aot_declaration *field(const char *class_name,const char *name) { + const gravity_aot_class *t=type(class_name); + for (uint32_t i=0; ifield_count; ++i) if (!strcmp(t->fields[i].declaration->name,name)) return t->fields[i].declaration; + assert(!"missing field"); return NULL; +} +static int flag(position *p,const char *name) { + if (!strcmp(name,"Active")) return p->active; + if (!strcmp(name,"Visible")) return p->visible; + if (!strcmp(name,"Paused")) return p->paused; + assert(!"unknown query filter"); return 0; +} +static int get(void *data,gravity_aot_value receiver,const char *key,gravity_aot_value *value) { + host_world *w=(host_world *)data; + int kind=((handle *)receiver.handle)->kind; + if (kind==POSITION) { + if (!strcmp(key,"Position")) { *value=receiver; return 1; } + if (!strcmp(key,"x")) { *value=gravity_aot_float(((position *)receiver.handle)->x); return 1; } + } + if (kind==CONTEXT) { + if (!strcmp(key,"deltaTime")) { *value=gravity_aot_float(0.5); return 1; } + if (!strcmp(key,"bonus")) { *value=gravity_aot_int(3); return 1; } + if (!strcmp(key,"world")) { *value=gravity_aot_host_ref(w->ctx,&w->world); return 1; } + } + return 0; +} +static int set(void *data,gravity_aot_value receiver,const char *key,gravity_aot_value value) { + (void)data; + if (((handle *)receiver.handle)->kind==POSITION && !strcmp(key,"x") && value.kind==GRAVITY_AOT_FLOAT) { + ((position *)receiver.handle)->x=value.floating; return 1; + } + return 0; +} +static int call(void *data,gravity_aot_value receiver,const char *key,const gravity_aot_value *args,uint32_t count,gravity_aot_value *result) { + host_world *w=(host_world *)data; + if (receiver.kind==GRAVITY_AOT_HOST && ((handle *)receiver.handle)->kind==WORLD && !strcmp(key,"notify") && count==1) { + w->notified=(int)args[0].integer; *result=gravity_aot_null(); return 1; + } + return 0; +} +static int next(void *data,gravity_aot_value receiver,uint64_t *state,gravity_aot_value *value) { + host_world *w=(host_world *)data; + assert(receiver.kind==GRAVITY_AOT_HOST && receiver.handle==&w->query); + const gravity_aot_attribute_value *with=arg(w->query_metadata,"with"), *without=arg(w->query_metadata,"without"); + while (*state<4) { + position *p=&w->positions[(*state)++]; int match=1; + for (uint32_t i=0; icount; ++i) if (!flag(p,with->items[i]->text)) match=0; + if (flag(p,without->text)) match=0; + if (match) { *value=gravity_aot_host_ref(w->ctx,p); return 1; } + } + return 0; +} +int main(void) { + unsigned char memory[32768]; + gravity_aot_arena arena={memory,sizeof(memory),0}; + gravity_aot_context c=gravity_aot_context_init(100000); + c.allocate=gravity_aot_arena_allocate; c.allocation_data=&arena; + const gravity_aot_module *module=attributes_get_module(); + assert(module->count==1 && module->type_count==8); + gravity_aot_value value=module->exports[0].call(&c,NULL,0); + assert(!c.error && !c.depth && value.kind==GRAVITY_AOT_FLOAT && value.floating==18.0); + printf("RESULT: %.0f\n",value.floating); + const gravity_aot_attribute *custom=attribute(module->exports[0].declaration,"metadata"); + const gravity_aot_attribute_value *items=arg(custom,"values"); + assert(items->kind==GRAVITY_AOT_ATTR_LIST && items->count==7); + assert(items->items[0]->kind==GRAVITY_AOT_ATTR_IDENTIFIER && !strcmp(items->items[0]->text,"Position")); + assert(items->items[1]->kind==GRAVITY_AOT_ATTR_STRING && !strcmp(items->items[1]->text,"text")); + assert(items->items[2]->integer==-3 && items->items[3]->floating==2.5 && items->items[4]->integer==1); + assert(items->items[5]->kind==GRAVITY_AOT_ATTR_NULL && !strcmp(items->items[6]->items[0]->text,"nested")); + assert(strstr(arg(custom,"note")->text,"\"") && custom->line>0 && module->exports[0].declaration->line>0); + assert(!strcmp(arg(attribute(type("Position")->declaration,"component"),"id")->text,"aot.position")); + assert(arg(attribute(type("Time")->declaration,"resource"),"autoInsert")->integer==1); + assert(arg(attribute(type("Health")->declaration,"replicated_component"),"version")->integer==2); + assert(arg(attribute(field("Health","value"),"network_field"),"tag")->integer==1); + assert(attribute(field("Health","dirty"),"local")->argument_count==0); + assert(attribute(type("Hit")->declaration,"network_command")); + assert(attribute(type("Tool")->declaration,"tool")); + assert(attribute(field("Controller","health"),"export")); + const gravity_aot_class *movement=type("Movement"); + assert(!strcmp(arg(attribute(movement->declaration,"system"),"scheduler")->text,"update")); + assert(!strcmp(arg(attribute(movement->declaration,"after"),"id")->text,"input")); + assert(!strcmp(arg(attribute(movement->declaration,"before"),"id")->text,"render")); + for (uint32_t i=0; imethod_count; ++i) + if (!strcmp(movement->methods[i].name,"damage")) { + const gravity_aot_declaration *d=movement->methods[i].declaration; + assert(attribute(d,"rpc") && d->parameter_count==1 && !strcmp(d->parameters[0].name,"amount") && !strcmp(d->parameters[0].type_name,"Int")); + } + host_world world={.positions={{POSITION,1,1,1,0},{POSITION,10,1,1,1},{POSITION,20,1,0,0},{POSITION,30,1,1,0}}, + .query={QUERY},.context={CONTEXT},.world={WORLD},.ctx=&c,.query_metadata=attribute(field("Movement","rows"),"query")}; + static const gravity_aot_host bridge={get,set,call,next}; c.host=&bridge; c.host_data=&world; + gravity_aot_value system=gravity_aot_construct(&c,movement,NULL,0); + gravity_aot_value timing=gravity_aot_construct(&c,type("Time"),NULL,0); + assert(gravity_aot_set(&c,system,"rows",gravity_aot_host_ref(&c,&world.query))); + assert(gravity_aot_set(&c,system,"timing",timing)); + gravity_aot_value context=gravity_aot_host_ref(&c,&world.context); + (void)gravity_aot_call(&c,system,"update",&context,1); + (void)gravity_aot_call(&c,system,"update",&context,1); + assert(!c.error && !c.depth && world.positions[0].x==3 && world.positions[3].x==32); + assert(world.positions[1].x==10 && world.positions[2].x==20 && world.notified==2); + gravity_aot_value damage=gravity_aot_int(4); + (void)gravity_aot_call(&c,system,"damage",&damage,1); + assert(!c.error && gravity_aot_get(&c,system,"calls").integer==6); + gravity_aot_value controller=gravity_aot_construct(&c,type("Controller"),NULL,0); + assert(gravity_aot_set(&c,controller,"position",gravity_aot_host_ref(&c,&world.positions[0]))); + assert(gravity_aot_set(&c,controller,"clock",timing)); + (void)gravity_aot_call(&c,controller,"ready",&context,1); + (void)gravity_aot_call(&c,controller,"update",&context,1); + (void)gravity_aot_call(&c,controller,"fixedUpdate",&context,1); + (void)gravity_aot_call(&c,controller,"event",&context,1); + assert(!c.error && gravity_aot_get(&c,controller,"health").integer==15 && world.positions[0].x==3.5); + gravity_aot_value label=gravity_aot_get(&c,controller,"label"); + assert(label.kind==GRAVITY_AOT_STRING && label.length==6 && !memcmp(label.string,"日本",6)); + (void)gravity_aot_call(&c,controller,"destroy",&context,1); + assert(!c.error && gravity_aot_get(&c,controller,"health").integer==0); + /* Borrowed field bindings must be refreshed after a host generation change. */ + ++c.host_generation; context=gravity_aot_host_ref(&c,&world.context); + (void)gravity_aot_call(&c,system,"update",&context,1); + assert(c.error==GRAVITY_AOT_STALE_HANDLE && !c.depth); + c.error=0; + (void)gravity_aot_call(&c,system,"missing",NULL,0); + assert(c.error==GRAVITY_AOT_METHOD && !c.depth); + c.error=0; + gravity_aot_get(&c,system,"missing"); assert(c.error==GRAVITY_AOT_FIELD); + gravity_aot_context tiny=gravity_aot_context_init(100); + gravity_aot_value failed=gravity_aot_construct(&tiny,movement,NULL,0); + assert(tiny.error==GRAVITY_AOT_MEMORY && failed.kind==GRAVITY_AOT_NULL); + puts("ATTRIBUTES: metadata, native systems/query filters/resources, RPC method and scriptable lifecycle passed"); + return 0; +} diff --git a/test/aot/host.c b/test/aot/host.c new file mode 100644 index 0000000..9e537b7 --- /dev/null +++ b/test/aot/host.c @@ -0,0 +1,48 @@ +#include "testmod.h" +#include +#include +#include + +static gravity_aot_function find(const char *name) { + const gravity_aot_module *m = testmod_get_module(); + assert(m->abi_version == GRAVITY_AOT_ABI_VERSION); + for (uint32_t i = 0; i < m->count; ++i) if (!strcmp(name, m->exports[i].name)) return m->exports[i].call; + assert(!"missing export"); return NULL; +} +int main(void) { + gravity_aot_context c = gravity_aot_context_init(10000); + gravity_aot_value v = find("main")(&c, NULL, 0); + assert(!c.error && !c.depth && v.kind == GRAVITY_AOT_INT && v.integer == 756); + printf("RESULT: %lld\n", (long long)v.integer); + v = find("wrap")(&c, NULL, 0); + assert(!c.error && v.integer == INT64_MIN); + gravity_aot_value args[] = {gravity_aot_int(9), gravity_aot_int(0)}; + find("divide")(&c, args, 2); + assert(c.error == GRAVITY_AOT_DIV_ZERO && !c.depth); + c = gravity_aot_context_init(100); + find("add")(&c, args, 1); + assert(c.error == GRAVITY_AOT_ARITY && !c.depth); + c = gravity_aot_context_init(100); + args[0] = gravity_aot_bool(1); + find("add")(&c, args, 2); + assert(c.error == GRAVITY_AOT_TYPE && !c.depth); + c = gravity_aot_context_init(100); + c.abi_version = 999; + find("empty")(&c, NULL, 0); + assert(c.error == GRAVITY_AOT_BAD_ABI && !c.depth); + c = gravity_aot_context_init(10); + find("infinite")(&c, NULL, 0); + assert(c.error == GRAVITY_AOT_LIMIT && !c.depth); + c = gravity_aot_context_init(10000); + c.max_depth = 3; + args[0] = gravity_aot_int(10); + find("fact")(&c, args, 1); + assert(c.error == GRAVITY_AOT_LIMIT && !c.depth); + c = gravity_aot_context_init(100); + args[0] = gravity_aot_bool(0); + v = find("logic")(&c, args, 1); + assert(!c.error && v.kind == GRAVITY_AOT_BOOL && v.integer == 1); + v = find("empty")(&c, NULL, 0); + assert(!c.error && v.kind == GRAVITY_AOT_NULL); + return 0; +} diff --git a/test/aot/run_all.py b/test/aot/run_all.py new file mode 100644 index 0000000..ad0882f --- /dev/null +++ b/test/aot/run_all.py @@ -0,0 +1,129 @@ +#!/usr/bin/env python3 +"""End-to-end native archive, differential VM and fail-closed AOT tests.""" +from pathlib import Path +import os +import re +import subprocess +import sys +import tempfile + +root = Path(__file__).resolve().parent.parent.parent +gravity = root / "gravity" +cc = os.environ.get("CC", "clang") +math_libraries = ["-lm"] if sys.platform != "win32" else [] + + +def run(args, **kwargs): + result = subprocess.run(args, capture_output=True, text=True, **kwargs) + if result.returncode: + print(result.stdout, file=sys.stderr) + print(result.stderr, file=sys.stderr) + result.check_returncode() + return result.stdout + + +with tempfile.TemporaryDirectory(prefix="gravity-aot-tests-") as tmp: + out = Path(tmp) + library = out / "libtestmod.a" + source = root / "test/aot/scalars.gravity" + run([sys.executable, str(root / "tools/aot_build.py"), str(source), "--module", "testmod", + "--cc", cc, "--output", str(library)]) + # The host links only the generated archive: no libgravity or VM. + host = out / "host" + run([cc, "-std=c99", "-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined", "-I", str(out), + str(root / "test/aot/host.c"), str(library), "-o", str(host)] + math_libraries) + native = run([str(host)]) + vm = run([str(gravity), str(source)]) + assert re.search(r"RESULT:\s*(?:\(INT\)\s*)?(-?\d+)", native).group(1) == re.search(r"RESULT:\s*(?:\(INT\)\s*)?(-?\d+)", vm).group(1), (native, vm) + # Instrument generated code too, not just the host. + run([cc, "-std=c99", "-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined", "-I", str(out), + str(root / "test/aot/host.c"), str(out / "testmod.c"), "-o", str(host)] + math_libraries) + run([str(host)]) + cases = [ + "class Player: Object {}", + "class Player { var x = [1,2]; }", + 'func main() { return ["key":1]; }', + "func main() { return { return 1; }; }", + "var global = 1; func main() { return global; }", + "class Outer { class Inner {} }", + "func main() { repeat {} while (false); }", + "func main(a = 1) { return a; }", + "class Player { static func main() { return 1; } }", + "func main() { var x = 1; func nested() { return x; } return nested(); }", + "func main() { @export var x = 1; return x; }", + "async func main() { return 1; }", + "func main() { return missing; }", + ] + for index, text in enumerate(cases): + bad = out / f"unsupported{index}.gravity" + bad.write_text(text) + generated = out / "unchanged.c" + generated.write_text("preserve existing output") + result = subprocess.run([str(gravity), "--emit-c", str(bad), "--module", "bad", "-o", str(generated)], capture_output=True) + assert result.returncode != 0, (text, result.stdout) + assert generated.read_text() == "preserve existing output", text + for prefix in ["bad-name", "0prefix", "_reserved", ""]: + result = subprocess.run([str(gravity), "--emit-c", str(source), "--module", prefix, "-o", str(out / "bad.c")], capture_output=True) + assert result.returncode != 0 + # Compare individual scalar semantics against the VM, including conversions. + expressions = ["1 + 2 * 3", "-7 / 3", "-7 % 3", "-(-4)", "true + 2", + "1 == true", "null == 0", "null + true", "null - 4", "null / 2", + "null % 2", "-null", "!null", "1 && 0", "false || 3", "1 < true", + "null >= false", "true != false", "9223372036854775807 + 1", + "1 - (-2)", "true * false", "null * 5", "0 != null", "+true"] + driver = out / "probe_host.c" + driver.write_text('''#include "probe.h" +#include +int main(void) { + gravity_aot_context c = gravity_aot_context_init(1000); + gravity_aot_value v = probe_get_module()->exports[0].call(&c, NULL, 0); + if (c.error) return 1; + if (v.kind == GRAVITY_AOT_BOOL) puts(v.integer ? "true" : "false"); + else if (v.kind == GRAVITY_AOT_NULL) puts("null"); + else if (v.kind == GRAVITY_AOT_FLOAT) printf("%.15g\\n", v.floating); + else if (v.kind == GRAVITY_AOT_STRING) printf("%.*s\\n", (int)v.length, v.string); + else printf("%lld\\n", (long long)v.integer); + return 0; +} +''') + expressions += ["1.5 + 2", "2 + 1.5", "1.5 * 2", "5 / 2.0", "5.5 % 2", "5 % 2.5", + "-1.5", "true + 2.5", "null - 2.5", "1 < 1.5", "1.0 == 1", "!0.0", + '"same" == "same"', '"a" < "b"', '"日本" == "日本"'] + error_expressions = ["null / 0", "null % 0", "1 / null", "1 % null", "1 / false", "0 / 0"] + for expression in expressions + error_expressions: + probe = out / "probe.gravity" + probe.write_text("func main() { return " + expression + "; }") + run([sys.executable, str(root / "tools/aot_build.py"), str(probe), "--module", "probe", + "--output", str(out / "libprobe.a"), "--cc", cc]) + run([cc, "-std=c99", "-fsanitize=address,undefined", "-I", str(out), str(driver), + str(out / "probe.c"), "-o", str(out / "probe_host")] + math_libraries) + vm_output = run([str(gravity), str(probe)]) + if expression in error_expressions: + assert subprocess.run([str(out / "probe_host")], capture_output=True).returncode == 1 + assert "RUNTIME ERROR:" in vm_output, (expression, vm_output) + continue + native_value = run([str(out / "probe_host")]).strip() + vm_match = re.search(r"RESULT:\s*(?:\(\w+\)\s*)?(\S+)", vm_output) + assert vm_match, (expression, vm_output) + vm_value = vm_match.group(1) + assert native_value == vm_value, (expression, native_value, vm_value) + # Class/struct metadata and native engine-style host callbacks. + attribute_source = root / "test/aot/attributes.gravity" + attribute_library = out / "libattributes.a" + run([sys.executable, str(root / "tools/aot_build.py"), str(attribute_source), "--module", "attributes", + "--output", str(attribute_library), "--cc", cc]) + attribute_host = out / "attributes_host" + run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined", "-I", str(out), + str(root / "test/aot/attributes_host.c"), str(attribute_library), "-o", str(attribute_host)] + math_libraries) + attribute_native = run([str(attribute_host)]) + attribute_vm = run([str(gravity), str(attribute_source)]) + assert re.search(r"RESULT: (\d+)", attribute_native).group(1) == re.search(r"RESULT: \(FLOAT\) (\d+)", attribute_vm).group(1) + run([cc, "-std=c11", "-Wall", "-Wextra", "-Werror", "-fsanitize=address,undefined", "-I", str(out), + str(root / "test/aot/attributes_host.c"), str(out / "attributes.c"), "-o", str(attribute_host)] + math_libraries) + run([str(attribute_host)]) + # A missing target SDK must leave a previously built library intact. + before = library.read_bytes() + failed = subprocess.run([sys.executable, str(root / "tools/aot_build.py"), str(source), "--module", "testmod", + "--output", str(library), "--sysroot", str(out / "missing-sdk")], capture_output=True) + assert failed.returncode != 0 and library.read_bytes() == before +print("AOT: native archive, VM comparison, sanitizers, ABI/errors/limits, 45 scalar comparisons and annotated native objects/host callbacks and 13 rejection cases passed") diff --git a/test/aot/scalars.gravity b/test/aot/scalars.gravity new file mode 100644 index 0000000..c6f37bf --- /dev/null +++ b/test/aot/scalars.gravity @@ -0,0 +1,25 @@ +func add(a: Int, b: Int) { return a + b; } +func sum(n: Int) { + var total = 0; + var i = 0; + while (i < n) { + i = i + 1; + if (i == 3) continue; + if (i > 8) break; + total = add(total, i); + } + return total; +} +func fact(n: Int) { + if (n <= 1) return 1; + return n * fact(n - 1); +} +func logic(x: Bool) { return !x || (3 < 4 && true); } +func wrap() { return 9223372036854775807 + 1; } +func divide(a: Int, b: Int) { return a / b; } +func infinite() { while (true) {} } +func empty() {} +func ordered() { + return add(add(1, 0), add(1, 1)); +} +func main() { return sum(12) + fact(6) + ordered(); } diff --git a/test/unittest/annotations_signed_values.gravity b/test/unittest/annotations_signed_values.gravity new file mode 100644 index 0000000..0a22d61 --- /dev/null +++ b/test/unittest/annotations_signed_values.gravity @@ -0,0 +1,15 @@ +#unittest { + name: "Signed annotation literals and event lifecycle method."; + error: NONE; + result: 7; +}; +@metadata(min: -3, max: +5, step: -0.5, nested: [-1, +2, [-3.5]]) +class Lifecycle { + var count = 4; + func event(context) { count = count + context; } +} +func main() { + var object = Lifecycle(); + object.event(3); + return object.count; +} diff --git a/tools/aot_build.py b/tools/aot_build.py new file mode 100644 index 0000000..7a1ef93 --- /dev/null +++ b/tools/aot_build.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python3 +"""Build a Gravity AOT library using the host compiler and target SDK.""" +import argparse +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tempfile + + +def main(): + root = Path(__file__).resolve().parent.parent + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("source", type=Path) + parser.add_argument("--module", required=True) + parser.add_argument("--output", required=True, type=Path, help="Output static archive (.a)") + parser.add_argument("--gravity", type=Path, default=root / "gravity", help="Host Gravity compiler") + parser.add_argument("--cc", default=os.environ.get("CC", "clang")) + parser.add_argument("--ar", default=os.environ.get("AR", "llvm-ar" if shutil.which("llvm-ar") else "ar")) + parser.add_argument("--target", help="Clang target triple") + parser.add_argument("--sysroot", type=Path, help="Target SDK/sysroot") + parser.add_argument("--cflag", action="append", default=[], help="Additional compiler flag; use --cflag=-flag") + args = parser.parse_args() + if not re.fullmatch(r"[A-Za-z][A-Za-z0-9_]*", args.module): + parser.error("module must be an ASCII identifier starting with a letter") + if args.module in ["gravity_aot_runtime", "gravity_aot_objects"]: + parser.error("module name conflicts with the standalone runtime header") + source = args.source.resolve(strict=True) + output = args.output.resolve() + if output == source or output.suffix != ".a": + parser.error("output must be a .a archive different from the source") + output.parent.mkdir(parents=True, exist_ok=True) + # No published artifacts change until all compiler/linker steps succeed. + with tempfile.TemporaryDirectory(prefix=".gravity-aot-", dir=output.parent) as staging: + stage = Path(staging) + c_file = stage / f"{args.module}.c" + header = stage / f"{args.module}.h" + runtime = stage / "gravity_aot_runtime.h" + obj = stage / f"{args.module}.o" + archive = stage / output.name + objects = stage / "gravity_aot_objects.h" + shutil.copyfile(root / "src/shared/gravity_aot_runtime.h", runtime) + shutil.copyfile(root / "src/shared/gravity_aot_objects.h", objects) + subprocess.run([str(args.gravity.resolve()), "--emit-c", str(source), "--module", args.module, + "-o", str(c_file)], check=True) + compile_args = [args.cc, "-std=c11", "-O2", "-fPIC", "-Wall", "-Wextra", "-Werror", "-I", str(stage)] + if args.target: + compile_args += ["--target=" + args.target] + if args.sysroot: + compile_args += ["--sysroot=" + str(args.sysroot.resolve(strict=True))] + subprocess.run(compile_args + args.cflag + ["-c", str(c_file), "-o", str(obj)], check=True) + subprocess.run([args.ar, "rcs", str(archive), str(obj)], check=True) + header.write_text(f'''/* Generated Gravity AOT module accessor. */ +#ifndef GRAVITY_AOT_MODULE_{args.module.upper()}_H +#define GRAVITY_AOT_MODULE_{args.module.upper()}_H +#include "gravity_aot_runtime.h" +#ifdef __cplusplus +extern "C" {{ +#endif +const gravity_aot_module *{args.module}_get_module(void); +#ifdef __cplusplus +}} +#endif +#endif +''') + for artifact in [c_file, header, runtime, objects, archive]: + os.replace(artifact, output.parent / artifact.name) + print(output) + + +if __name__ == "__main__": + try: + main() + except (OSError, subprocess.CalledProcessError) as error: + print(f"AOT build failed: {error}", file=sys.stderr) + sys.exit(1)