diff --git a/Ubytec.Tests/SourceSemanticFrontendTests.cs b/Ubytec.Tests/SourceSemanticFrontendTests.cs new file mode 100644 index 00000000..9015c224 --- /dev/null +++ b/Ubytec.Tests/SourceSemanticFrontendTests.cs @@ -0,0 +1,155 @@ +using Ubytec.Language.AST; +using Ubytec.Language.Exceptions; +using Ubytec.Language.Portable; +using Xunit; + +namespace Ubytec.Tests; + +public sealed class SourceSemanticFrontendTests +{ + private const string ContractSource = """ + import Remote from math.Twice (t_int64) -> t_int64 + export Main as Execute (t_int64) -> t_int64 + module (name:"semantic-front", version:"0.1", author:"Ubytec") + { + func Main(t_int64 value) -> t_int64 + { + load @value + push 1000 + push 16384 + call Remote + return + } + } + """; + + [Fact] + public void SemanticFrontendProducesTheSameCanonicalPortableProgramAsLegacyCompilation() + { + SourceSemanticCompilation semantic = SourceCompiler.CompileSemantic( + ContractSource, + processImage: false, + enableModuleCalls: true); + SourceCompilation legacy = SourceCompiler.Compile( + ContractSource, + emitProcessEntryPoint: false, + enableModuleCalls: true); + + byte[] semanticBytes = PortableModuleFormat.Encode( + semantic.Module, + contractManifest: null, + includeIndexedExecutable: true, + portableContracts: semantic.PortableContracts); + byte[] legacyBytes = PortableModuleFormat.Encode( + legacy.Module, + contractManifest: null, + includeIndexedExecutable: true, + portableContracts: legacy.PortableContracts); + + Assert.Equal(legacyBytes, semanticBytes); + } + + [Fact] + public void SemanticCompilationExposesNoNativeArtifactSurface() + { + Type resultType = typeof(SourceSemanticCompilation); + + Assert.Null(resultType.GetProperty("Assembly")); + Assert.Null(resultType.GetProperty("EntryLabel")); + Assert.Equal( + ["Module", "PortableContracts"], + resultType.GetProperties() + .Select(property => property.Name) + .OrderBy(name => name, StringComparer.Ordinal) + .ToArray()); + } + + [Fact] + public void SemanticFrontendDoesNotInvokeLegacyProcessEntrypointEmitter() + { + const string source = """ + module (name:"semantic-no-native", version:"0.1", author:"Ubytec") + { + func Main(t_int64 value) -> t_int64 + { + load @value + return + } + } + """; + + SourceSemanticCompilation semantic = SourceCompiler.CompileSemantic(source, processImage: true); + + Assert.Equal("semantic-no-native", semantic.Module.Name); + Assert.Throws(() => SourceCompiler.Compile(source, emitProcessEntryPoint: true)); + } + + [Fact] + public void SemanticFrontendStillRejectsEvaluationStackUnderflow() + { + const string source = """ + module (name:"semantic-underflow", version:"0.1", author:"Ubytec") + { + func Main() -> t_int64 + { + add + return + } + } + """; + + SyntaxException error = Assert.Throws(() => + SourceCompiler.CompileSemantic(source)); + + Assert.Contains("needs 2 stack cell", error.Message, StringComparison.Ordinal); + } + + [Fact] + public void SemanticFrontendStillRejectsStoreToReadonlyFrameBinding() + { + const string source = """ + module (name:"semantic-readonly", version:"0.1", author:"Ubytec") + { + func Main() -> t_int64 + { + local + { + readonly t_int64 value 1 + } + push 2 + store @value + load @value + return + } + } + """; + + SyntaxException error = Assert.Throws(() => + SourceCompiler.CompileSemantic(source)); + + Assert.Contains("readonly symbol", error.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void PortableProcessPipelineUsesProcessContractRulesWithoutLegacyCodegen() + { + const string exportedProcess = """ + export Main as Execute () -> t_int64 + module (name:"portable-process-contract", version:"0.1", author:"Ubytec") + { + func Main() -> t_int64 + { + push 42 + return + } + } + """; + + Assert.Throws(() => PortableSourceX64Pipeline.Compile(exportedProcess)); + + PortableSourceX64Result library = PortableSourceX64Pipeline.Compile( + exportedProcess, + processImage: false); + Assert.Null(PortableProcessEntrypoint.Read(PortableBinaryCodec.Decode(library.ModuleBytes))); + } +} diff --git a/Ubytec/Language/AST/SourceCompiler.cs b/Ubytec/Language/AST/SourceCompiler.cs index cae5fb24..a5c54ef7 100644 --- a/Ubytec/Language/AST/SourceCompiler.cs +++ b/Ubytec/Language/AST/SourceCompiler.cs @@ -6,14 +6,23 @@ using Ubytec.Language.Grammar; using Ubytec.Language.HighLevel; using Ubytec.Language.Operations; +using Ubytec.Language.Portable; using Ubytec.Language.Syntax.Scopes; +using Ubytec.Language.Syntax.Scopes.Contexts; using Ubytec.Language.Syntax.TypeSystem; namespace Ubytec.Language.AST; +/// A parsed and validated source module with architecture-neutral portable contracts. +/// Normalized semantic module. +/// Portable import/export identities and signatures. +public sealed record SourceSemanticCompilation( + Module Module, + PortableModuleContracts PortableContracts); + /// A source module and its generated Linux x86-64 NASM assembly. /// Parsed module. -/// Assembly emitted by the module compiler. +/// Assembly emitted by the legacy source backend. /// Main function's internal entry label, if present. public sealed record SourceCompilation(Module Module, string Assembly, string? EntryLabel) { @@ -24,8 +33,7 @@ public sealed record SourceCompilation(Module Module, string Assembly, string? E /// Semantic import/export contracts retained before host handles and adapter symbols are introduced. /// Portable lowering uses this view instead of reverse-engineering the legacy textual manifest. /// - public Ubytec.Language.Portable.PortableModuleContracts PortableContracts { get; internal set; } = - Ubytec.Language.Portable.PortableModuleContracts.Empty; + public PortableModuleContracts PortableContracts { get; internal set; } = PortableModuleContracts.Empty; /// Adapts up to six integer scalar arguments from Linux System V to the Ubytec stack ABI. public string GetLinuxSystemVAdapter(string functionName, string exportedName) @@ -59,12 +67,40 @@ public string GetFunctionLabel(string name) } } -/// Complete source-to-assembly entry point using the bundled grammar. +/// +/// Source frontend plus the retained legacy source-to-NASM entry point. +/// Portable backends should consume and cross the UBCM boundary before native code generation. +/// public static class SourceCompiler { private static readonly object Gate = new(); - /// Tokenizes, parses, validates and compiles one source module. + /// + /// Tokenizes, parses and validates one source module without invoking any native backend. + /// selects process-vs-library source contract rules only; no `_start` or NASM is emitted. + /// + public static SourceSemanticCompilation CompileSemantic( + string source, + string? grammarJson = null, + bool processImage = true, + bool enableModuleCalls = false, + bool enableHostIO = false) + { + ValidateCompilationMode(processImage, enableModuleCalls, enableHostIO); + return InInvariantContext(() => + { + ParsedSource parsed = ParseSource( + source, + grammarJson, + processImage, + enableModuleCalls, + enableHostIO); + ValidateSemanticModule(parsed, enableModuleCalls, enableHostIO); + return new SourceSemanticCompilation(parsed.Module, parsed.PortableContracts); + }); + } + + /// Tokenizes, parses, validates and compiles one source module through the legacy x64 NASM backend. /// Ubytec module source. /// Optional explicit TextMate grammar JSON. /// False emits a library without _start. @@ -82,11 +118,43 @@ public static class SourceCompiler /// Keep write-through frame copies through branches and loops, reloading after raw stores and calls. public static SourceCompilation Compile(string source, string? grammarJson = null, bool emitProcessEntryPoint = true, bool optimizeTailCalls = false, bool enableModuleCalls = false, bool enableHostIO = false, bool optimizeStackTransfers = false, bool optimizeFrameValues = false, bool optimizeFrameRegisters = false, bool optimizeLoopReductions = false, bool optimizeBlockReductions = false, bool optimizeNativeBlockReductions = false, bool optimizeRecordReductions = false, bool optimizeLookupReductions = false, bool optimizeEffectRegisters = false) { - if (enableModuleCalls && emitProcessEntryPoint) + ValidateCompilationMode(emitProcessEntryPoint, enableModuleCalls, enableHostIO); + return InInvariantContext(() => + { + ParsedSource parsed = ParseSource( + source, + grammarJson, + emitProcessEntryPoint, + enableModuleCalls, + enableHostIO); + return CompileNative( + parsed, + emitProcessEntryPoint, + optimizeTailCalls, + enableModuleCalls, + enableHostIO, + optimizeStackTransfers, + optimizeFrameValues, + optimizeFrameRegisters, + optimizeLoopReductions, + optimizeBlockReductions, + optimizeNativeBlockReductions, + optimizeRecordReductions, + optimizeLookupReductions, + optimizeEffectRegisters); + }); + } + + private static void ValidateCompilationMode(bool processImage, bool enableModuleCalls, bool enableHostIO) + { + if (enableModuleCalls && processImage) throw new ArgumentException("Module calls require library compilation."); - if (enableHostIO && (!enableModuleCalls || emitProcessEntryPoint)) + if (enableHostIO && (!enableModuleCalls || processImage)) throw new ArgumentException("Host I/O requires library compilation and module calls."); + } + private static T InInvariantContext(Func action) + { lock (Gate) { CultureInfo previousCulture = CultureInfo.CurrentCulture; @@ -96,122 +164,7 @@ public static SourceCompilation Compile(string source, string? grammarJson = nul try { - source = source.ReplaceLineEndings("\n"); - grammarJson = grammarJson?.ReplaceLineEndings("\n"); - - var contracts = new ModuleContracts(); - source = contracts.Read(source).ReplaceLineEndings("\n"); - if (contracts.Imports.Count != 0 && !enableModuleCalls) - throw new ArgumentException("Imports require --module-calls."); - if (contracts.Exports.Count != 0 && emitProcessEntryPoint) - throw new ArgumentException("Exports require library compilation."); - - LexicalAnalyst.InitializeGrammar(grammarJson); - var tokens = LexicalAnalyst.Tokenize(source); - var (module, errors) = HighLevelParser.ParseModule([.. tokens]); - if (errors.Length != 0) - throw new SyntaxException(0xBAD076, string.Join("\n", - errors.Select(e => $"line {e.Line + 1}: {e.Where}: {e.Message}"))); - - module = SemanticIdentity.Normalize(module); - - var scopes = new CompilationScopes - { - EmitProcessEntryPoint = emitProcessEntryPoint, - OptimizeTailCalls = optimizeTailCalls, - OptimizeFrameRegisters = optimizeFrameRegisters, - OptimizeLoopReductions = optimizeLoopReductions, - OptimizeBlockReductions = optimizeBlockReductions || optimizeNativeBlockReductions, - UseAvx2BlockReductions = optimizeNativeBlockReductions && Avx2.IsSupported, - OptimizeRecordReductions = optimizeRecordReductions, - OptimizeLookupReductions = optimizeLookupReductions, - OptimizeEffectRegisters = optimizeEffectRegisters - }; - - if (enableModuleCalls) - { - if (module.Functions.Any(f => f.Name == "ModuleInvoke" || contracts.Imports.Any(i => i.Alias == f.Name)) || - module.Actions.Any(a => a.Name == "ModuleInvoke" || contracts.Imports.Any(i => i.Alias == a.Name))) - throw new ArgumentException("ModuleInvoke is reserved when module calls are enabled."); - - var imports = new Ubytec.Language.Syntax.Scopes.Contexts.ScopeContext { DeclaredByKeyword = "host-imports" }; - imports.Callables.Add("ModuleInvoke", new("ModuleInvoke", "ubytec_module_invoke", 6, - new Types.UType(Types.PrimitiveType.Int64))); - foreach (var import in contracts.Imports) - imports.Callables.Add(import.Alias, new(import.Alias, "ubytec_import_" + import.Alias, import.CallArguments, - new Types.UType(Types.PrimitiveType.Int64))); - - if (enableHostIO) - { - foreach (string name in new[] { "HostRead", "HostWrite" }) - { - if (module.Functions.Any(f => f.Name == name) || - module.Actions.Any(a => a.Name == name) || - contracts.Imports.Any(i => i.Alias == name)) - throw new ArgumentException(name + " is reserved when host I/O is enabled."); - imports.Callables.Add(name, new(name, "ubytec_hostio_" + name, 5, - new Types.UType(Types.PrimitiveType.Int64))); - } - } - scopes.Push(imports); - } - - string assembly = module.Compile(scopes).ReplaceLineEndings("\n"); - - if (enableModuleCalls) assembly += """ - - section .text - extern ubytec_bridge_call - ubytec_module_invoke: - push rbp - mov rbp, rsp - mov rdi, [rbp+56] - mov rsi, [rbp+48] - mov rdx, [rbp+40] - mov rcx, [rbp+32] - mov r8, [rbp+24] - mov r9, [rbp+16] - and rsp, -16 - call ubytec_bridge_call - mov rsp, rbp - pop rbp - ret - - """; - - if (enableHostIO) - { - foreach (string name in new[] { "HostRead", "HostWrite" }) - { - string bridge = name == "HostRead" ? "ubytec_bridge_read" : "ubytec_bridge_write"; - assembly += $"\nsection .text\nextern {bridge}\nubytec_hostio_{name}:\n" + - "push rbp\nmov rbp, rsp\nmov rdi, [rbp+48]\nmov rsi, [rbp+40]\n" + - "mov rdx, [rbp+32]\nmov rcx, [rbp+24]\nmov r8, [rbp+16]\n" + - $"and rsp, -16\ncall {bridge}\nmov rsp, rbp\npop rbp\nret\n"; - } - } - - if (optimizeStackTransfers) - assembly = Ubytec.Language.Tools.Optimization.StackTransferOptimizer.Optimize(assembly); - if (optimizeFrameValues) - assembly = Ubytec.Language.Tools.Optimization.FrameValueOptimizer.Optimize(assembly); - - assembly = assembly.ReplaceLineEndings("\n"); - - var main = module.Functions.FirstOrDefault(x => x.Name == "Main"); - var result = new SourceCompilation( - module, - assembly, - string.IsNullOrEmpty(main.Name) ? null : FunctionEmitter.Binding(main).Label); - - if (contracts.Imports.Count != 0 || contracts.Exports.Count != 0) - { - string extra = contracts.Emit(result); - result = result with { Assembly = (assembly + extra).ReplaceLineEndings("\n") }; - } - - result.PortableContracts = Ubytec.Language.Portable.PortableModuleContracts.From(contracts, module); - return result; + return action(); } finally { @@ -220,4 +173,238 @@ pop rbp } } } + + private static ParsedSource ParseSource( + string source, + string? grammarJson, + bool processImage, + bool enableModuleCalls, + bool enableHostIO) + { + source = source.ReplaceLineEndings("\n"); + grammarJson = grammarJson?.ReplaceLineEndings("\n"); + + var contracts = new ModuleContracts(); + source = contracts.Read(source).ReplaceLineEndings("\n"); + if (contracts.Imports.Count != 0 && !enableModuleCalls) + throw new ArgumentException("Imports require --module-calls."); + if (contracts.Exports.Count != 0 && processImage) + throw new ArgumentException("Exports require library compilation."); + + LexicalAnalyst.InitializeGrammar(grammarJson); + var tokens = LexicalAnalyst.Tokenize(source); + var (module, errors) = HighLevelParser.ParseModule([.. tokens]); + if (errors.Length != 0) + { + throw new SyntaxException(0xBAD076, string.Join("\n", + errors.Select(e => $"line {e.Line + 1}: {e.Where}: {e.Message}"))); + } + + module = SemanticIdentity.Normalize(module); + ValidateReservedNames(module, contracts, enableModuleCalls, enableHostIO); + PortableModuleContracts portableContracts = PortableModuleContracts.From(contracts, module); + return new ParsedSource(module, contracts, portableContracts); + } + + private static void ValidateReservedNames( + Module module, + ModuleContracts contracts, + bool enableModuleCalls, + bool enableHostIO) + { + if (!enableModuleCalls) + return; + + if (module.Functions.Any(f => f.Name == "ModuleInvoke" || contracts.Imports.Any(i => i.Alias == f.Name)) || + module.Actions.Any(a => a.Name == "ModuleInvoke" || contracts.Imports.Any(i => i.Alias == a.Name))) + throw new ArgumentException("ModuleInvoke is reserved when module calls are enabled."); + + if (!enableHostIO) + return; + + foreach (string name in new[] { "HostRead", "HostWrite" }) + { + if (module.Functions.Any(f => f.Name == name) || + module.Actions.Any(a => a.Name == name) || + contracts.Imports.Any(i => i.Alias == name)) + throw new ArgumentException(name + " is reserved when host I/O is enabled."); + } + } + + private static void ValidateSemanticModule( + ParsedSource parsed, + bool enableModuleCalls, + bool enableHostIO) + { + Module module = parsed.Module; + module.Validate(); + + var scopes = new CompilationScopes { EmitProcessEntryPoint = false }; + if (enableModuleCalls) + scopes.Push(CreateHostImportScope(parsed.Contracts, enableHostIO)); + + var moduleScope = new ScopeContext + { + StartLabel = FunctionCode.Label("module", module.Name, module.ID, "start"), + EndLabel = FunctionCode.Label("module", module.Name, module.ID, "end"), + DeclaredByKeyword = "module" + }; + foreach (var function in module.Functions) + moduleScope.Callables.Add(function.Name, FunctionEmitter.Binding(function)); + foreach (var action in module.Actions) + moduleScope.Callables.Add(action.Name, FunctionEmitter.Binding(action)); + + int originalCount = scopes.Count; + scopes.Push(moduleScope); + try + { + foreach (var function in module.Functions) + { + FunctionEmitter.Validate( + function.Name, + function.ID, + "func", + function.Arguments, + function.Locals, + function.Definition, + function.ReturnType, + scopes); + } + foreach (var action in module.Actions) + { + FunctionEmitter.Validate( + action.Name, + action.ID, + "action", + action.Arguments, + action.Locals, + action.Definition, + new Types.UType(Types.PrimitiveType.Void), + scopes); + } + } + finally + { + while (scopes.Count > originalCount) scopes.Pop(); + } + } + + private static ScopeContext CreateHostImportScope(ModuleContracts contracts, bool enableHostIO) + { + var imports = new ScopeContext { DeclaredByKeyword = "host-imports" }; + imports.Callables.Add("ModuleInvoke", new("ModuleInvoke", "ubytec_module_invoke", 6, + new Types.UType(Types.PrimitiveType.Int64))); + foreach (var import in contracts.Imports) + { + imports.Callables.Add(import.Alias, new(import.Alias, "ubytec_import_" + import.Alias, import.CallArguments, + new Types.UType(Types.PrimitiveType.Int64))); + } + + if (enableHostIO) + { + foreach (string name in new[] { "HostRead", "HostWrite" }) + { + imports.Callables.Add(name, new(name, "ubytec_hostio_" + name, 5, + new Types.UType(Types.PrimitiveType.Int64))); + } + } + return imports; + } + + private static SourceCompilation CompileNative( + ParsedSource parsed, + bool emitProcessEntryPoint, + bool optimizeTailCalls, + bool enableModuleCalls, + bool enableHostIO, + bool optimizeStackTransfers, + bool optimizeFrameValues, + bool optimizeFrameRegisters, + bool optimizeLoopReductions, + bool optimizeBlockReductions, + bool optimizeNativeBlockReductions, + bool optimizeRecordReductions, + bool optimizeLookupReductions, + bool optimizeEffectRegisters) + { + Module module = parsed.Module; + ModuleContracts contracts = parsed.Contracts; + var scopes = new CompilationScopes + { + EmitProcessEntryPoint = emitProcessEntryPoint, + OptimizeTailCalls = optimizeTailCalls, + OptimizeFrameRegisters = optimizeFrameRegisters, + OptimizeLoopReductions = optimizeLoopReductions, + OptimizeBlockReductions = optimizeBlockReductions || optimizeNativeBlockReductions, + UseAvx2BlockReductions = optimizeNativeBlockReductions && Avx2.IsSupported, + OptimizeRecordReductions = optimizeRecordReductions, + OptimizeLookupReductions = optimizeLookupReductions, + OptimizeEffectRegisters = optimizeEffectRegisters + }; + + if (enableModuleCalls) + scopes.Push(CreateHostImportScope(contracts, enableHostIO)); + + string assembly = module.Compile(scopes).ReplaceLineEndings("\n"); + + if (enableModuleCalls) assembly += """ + + section .text + extern ubytec_bridge_call + ubytec_module_invoke: + push rbp + mov rbp, rsp + mov rdi, [rbp+56] + mov rsi, [rbp+48] + mov rdx, [rbp+40] + mov rcx, [rbp+32] + mov r8, [rbp+24] + mov r9, [rbp+16] + and rsp, -16 + call ubytec_bridge_call + mov rsp, rbp + pop rbp + ret + + """; + + if (enableHostIO) + { + foreach (string name in new[] { "HostRead", "HostWrite" }) + { + string bridge = name == "HostRead" ? "ubytec_bridge_read" : "ubytec_bridge_write"; + assembly += $"\nsection .text\nextern {bridge}\nubytec_hostio_{name}:\n" + + "push rbp\nmov rbp, rsp\nmov rdi, [rbp+48]\nmov rsi, [rbp+40]\n" + + "mov rdx, [rbp+32]\nmov rcx, [rbp+24]\nmov r8, [rbp+16]\n" + + $"and rsp, -16\ncall {bridge}\nmov rsp, rbp\npop rbp\nret\n"; + } + } + + if (optimizeStackTransfers) + assembly = Ubytec.Language.Tools.Optimization.StackTransferOptimizer.Optimize(assembly); + if (optimizeFrameValues) + assembly = Ubytec.Language.Tools.Optimization.FrameValueOptimizer.Optimize(assembly); + + assembly = assembly.ReplaceLineEndings("\n"); + + var main = module.Functions.FirstOrDefault(x => x.Name == "Main"); + var result = new SourceCompilation( + module, + assembly, + string.IsNullOrEmpty(main.Name) ? null : FunctionEmitter.Binding(main).Label); + + if (contracts.Imports.Count != 0 || contracts.Exports.Count != 0) + { + string extra = contracts.Emit(result); + result = result with { Assembly = (assembly + extra).ReplaceLineEndings("\n") }; + } + + result.PortableContracts = parsed.PortableContracts; + return result; + } + + private sealed record ParsedSource( + Module Module, + ModuleContracts Contracts, + PortableModuleContracts PortableContracts); } diff --git a/Ubytec/Language/Operations/FunctionEmitter.cs b/Ubytec/Language/Operations/FunctionEmitter.cs index 8dbd26d4..47a6db65 100644 --- a/Ubytec/Language/Operations/FunctionEmitter.cs +++ b/Ubytec/Language/Operations/FunctionEmitter.cs @@ -34,54 +34,41 @@ IEnumerable Visit(SyntaxNode node) foreach (var op in Instructions(child)) yield return op; } - internal static string Compile(string name, Guid id, string kind, Argument[] arguments, LocalContext? locals, + /// + /// Validates one callable and its nested callables without emitting native code. + /// The checks are shared with the legacy emitter so semantic and native paths cannot drift. + /// + internal static void Validate(string name, Guid id, string kind, Argument[] arguments, LocalContext? locals, SyntaxSentence? definition, UType returnType, CompilationScopes scopes) { - if (definition is null) throw new SyntaxException(0xBAD06B, $"'{name}' has no executable body."); - if (returnType.Type != PrimitiveType.Void && !FunctionCode.IsCellType(returnType)) - throw new SyntaxException(0xBAD06C, $"Return type {returnType} is not supported by the cell ABI."); - var frame = new ScopeContext - { - StartLabel = FunctionCode.Label(kind, name, id, "start"), - EndLabel = FunctionCode.Label(kind, name, id, "end"), - DeclaredByKeyword = kind, - ExpectedReturnType = returnType, - ArgumentCount = arguments.Length - }; - frame.Callables.Add(name, new(name, frame.StartLabel, arguments.Length, returnType)); - void Add(StackBinding binding) + ScopeContext frame = CreateFrame(name, id, kind, arguments, locals, definition, returnType); + int originalCount = scopes.Count; + scopes.Push(frame); + try { - if (!FunctionCode.IsCellType(binding.Type)) - throw new SyntaxException(0xBAD06E, $"Unsupported frame type for '{binding.Name}': {binding.Type}."); - if (!frame.Symbols.TryAdd(binding.Name, binding)) - throw new SyntaxException(0xBAD06F, $"Duplicate frame symbol '{binding.Name}'."); + ValidateCurrentBody(definition!, locals, returnType, scopes); + foreach (var nested in locals?.Functions ?? []) + Validate(nested.Name, nested.ID, "func", nested.Arguments, nested.Locals, nested.Definition, nested.ReturnType, scopes); + foreach (var nested in locals?.Actions ?? []) + Validate(nested.Name, nested.ID, "action", nested.Arguments, nested.Locals, nested.Definition, new UType(PrimitiveType.Void), scopes); } - for (int i = 0; i < arguments.Length; i++) + finally { - var arg = arguments[i]; - Add(new(arg.Name, arg.Type, 16 + (arguments.Length - 1 - i) * 8, - arg.Modifiers.HasFlag(TypeModifiers.ReadOnly))); + while (scopes.Count > originalCount) scopes.Pop(); } - int slots = 0; - foreach (var local in locals?.Variables ?? []) - Add(new(local.Name, local.Type, -8 * ++slots, - local.Modifiers.HasFlag(TypeModifiers.ReadOnly) || local.Modifiers.HasFlag(TypeModifiers.Const))); - foreach (VAR op in Instructions(definition).OfType()) - Add(new(op.Variable.Name, op.Variable.Type, -8 * ++slots, - op.Variable.Type.Modifiers.HasFlag(TypeModifiers.ReadOnly) || op.Variable.Type.Modifiers.HasFlag(TypeModifiers.Const))); - foreach (var selection in Instructions(definition).OfType()) frame.SwitchSlots.Enqueue(-8 * ++slots); - frame.FrameSize = (slots * 8 + 15) & ~15; - - foreach (var nested in locals?.Functions ?? []) frame.Callables.Add(nested.Name, Binding(nested)); - foreach (var nested in locals?.Actions ?? []) frame.Callables.Add(nested.Name, Binding(nested)); + } + internal static string Compile(string name, Guid id, string kind, Argument[] arguments, LocalContext? locals, + SyntaxSentence? definition, UType returnType, CompilationScopes scopes) + { + ScopeContext frame = CreateFrame(name, id, kind, arguments, locals, definition, returnType); int originalCount = scopes.Count; scopes.Push(frame); try { - FunctionStackValidator.Validate(definition, scopes, returnType); + ValidateCurrentBody(definition!, locals, returnType, scopes); var valueIR = scopes.OptimizeLoopReductions || scopes.OptimizeBlockReductions || scopes.OptimizeRecordReductions || scopes.OptimizeLookupReductions || scopes.OptimizeFrameRegisters || scopes.OptimizeEffectRegisters - ? Ubytec.Language.Tools.Optimization.FunctionValueIR.Lower(definition) : null; + ? Ubytec.Language.Tools.Optimization.FunctionValueIR.Lower(definition!) : null; var reduction = scopes.OptimizeLoopReductions || scopes.OptimizeBlockReductions ? Ubytec.Language.Tools.Optimization.ByteReductionPlan.TryCreate(valueIR!, frame, arguments, locals, returnType) : null; @@ -114,9 +101,9 @@ void Add(StackBinding binding) foreach (var binding in frame.Symbols.Values.Where(b => b.Register is not null)) output.AppendLine($"mov {binding.Register}, qword {binding.Address}"); } - // Compile the reference body even when replacing it: its scope/type/readonly - // validation and return tracking must run with exactly the original rules. - string body = ASTCompiler.CompileAST(new SyntaxTree(definition), scopes); + // Compile the reference body even when replacing it: native scope/control validation and + // return tracking must continue to run with exactly the original backend rules. + string body = ASTCompiler.CompileAST(new SyntaxTree(definition!), scopes); output.Append(lookupReduction is not null ? lookupReduction.Emit(frame) : recordReduction is not null ? recordReduction.Emit(frame) : reduction is null ? body : scopes.OptimizeBlockReductions ? Ubytec.Language.Tools.Optimization.BlockByteReductionPlan.Emit(reduction, frame, scopes.UseAvx2BlockReductions) : reduction.Emit(frame)); @@ -134,4 +121,96 @@ void Add(StackBinding binding) while (scopes.Count > originalCount) scopes.Pop(); } } + + private static ScopeContext CreateFrame( + string name, + Guid id, + string kind, + Argument[] arguments, + LocalContext? locals, + SyntaxSentence? definition, + UType returnType) + { + if (definition is null) throw new SyntaxException(0xBAD06B, $"'{name}' has no executable body."); + if (returnType.Type != PrimitiveType.Void && !FunctionCode.IsCellType(returnType)) + throw new SyntaxException(0xBAD06C, $"Return type {returnType} is not supported by the cell ABI."); + + var frame = new ScopeContext + { + StartLabel = FunctionCode.Label(kind, name, id, "start"), + EndLabel = FunctionCode.Label(kind, name, id, "end"), + DeclaredByKeyword = kind, + ExpectedReturnType = returnType, + ArgumentCount = arguments.Length + }; + frame.Callables.Add(name, new(name, frame.StartLabel, arguments.Length, returnType)); + + void Add(StackBinding binding) + { + if (!FunctionCode.IsCellType(binding.Type)) + throw new SyntaxException(0xBAD06E, $"Unsupported frame type for '{binding.Name}': {binding.Type}."); + if (!frame.Symbols.TryAdd(binding.Name, binding)) + throw new SyntaxException(0xBAD06F, $"Duplicate frame symbol '{binding.Name}'."); + } + + for (int i = 0; i < arguments.Length; i++) + { + var arg = arguments[i]; + Add(new(arg.Name, arg.Type, 16 + (arguments.Length - 1 - i) * 8, + arg.Modifiers.HasFlag(TypeModifiers.ReadOnly))); + } + + int slots = 0; + foreach (var local in locals?.Variables ?? []) + Add(new(local.Name, local.Type, -8 * ++slots, + local.Modifiers.HasFlag(TypeModifiers.ReadOnly) || local.Modifiers.HasFlag(TypeModifiers.Const))); + foreach (VAR op in Instructions(definition).OfType()) + Add(new(op.Variable.Name, op.Variable.Type, -8 * ++slots, + op.Variable.Type.Modifiers.HasFlag(TypeModifiers.ReadOnly) || op.Variable.Type.Modifiers.HasFlag(TypeModifiers.Const))); + foreach (var selection in Instructions(definition).OfType()) frame.SwitchSlots.Enqueue(-8 * ++slots); + frame.FrameSize = (slots * 8 + 15) & ~15; + + foreach (var nested in locals?.Functions ?? []) frame.Callables.Add(nested.Name, Binding(nested)); + foreach (var nested in locals?.Actions ?? []) frame.Callables.Add(nested.Name, Binding(nested)); + return frame; + } + + private static void ValidateCurrentBody( + SyntaxSentence definition, + LocalContext? locals, + UType returnType, + CompilationScopes scopes) + { + FunctionStackValidator.Validate(definition, scopes, returnType); + + foreach (var local in locals?.Variables ?? []) + _ = FunctionCode.InitialValue(local.Type, local.Value); + + foreach (IOpCode operation in Instructions(definition)) + { + switch (operation) + { + case VAR declaration: + _ = FunctionCode.InitialValue(declaration.Variable.Type, declaration.Variable.Value); + break; + + case RETURN { BlockType: UType actual }: + if (actual.Type != returnType.Type || actual.Modifiers != returnType.Modifiers) + throw new SyntaxException(0xBAD073, $"RETURN type {actual} differs from function type {returnType}."); + break; + + case MemoryOperations.LOAD { VariableName: { } name }: + _ = scopes.ResolveSymbol(name); + break; + + case MemoryOperations.STORE { VariableName: { } name }: + { + StackBinding binding = scopes.ResolveSymbol(name); + if (binding.ReadOnly) + throw new SyntaxException(0xBAD064, $"Cannot store to readonly symbol '{binding.Name}'."); + break; + } + } + } + } } diff --git a/Ubytec/Language/Portable/PortableSourceX64Pipeline.cs b/Ubytec/Language/Portable/PortableSourceX64Pipeline.cs index 9e0da121..aaf19635 100644 --- a/Ubytec/Language/Portable/PortableSourceX64Pipeline.cs +++ b/Ubytec/Language/Portable/PortableSourceX64Pipeline.cs @@ -10,16 +10,15 @@ public sealed record PortableSourceX64Result( Module SemanticModule); /// -/// Transitional canonical pipeline that derives final x64 solely from indexed UBCM bytes. -/// SourceCompiler still computes its legacy NASM internally; this pipeline deliberately discards it. -/// No legacy assembly contributes to . +/// Canonical source -> portable binary -> x64 pipeline. The source phase is semantic only: +/// no legacy NASM is generated before the verified UBCM boundary is crossed. /// public static class PortableSourceX64Pipeline { /// - /// Compiles source to indexed PMF and then generates x64 only from the resulting verified binary. + /// Compiles source semantically to indexed PMF and then generates x64 only from the resulting verified binary. /// The returned semantic module is exposed only for diagnostics and sidecars; native code generation - /// has already crossed the verified UBCM boundary before this method returns. + /// depends exclusively on . /// public static PortableSourceX64Result Compile( string source, @@ -28,10 +27,10 @@ public static PortableSourceX64Result Compile( bool enableModuleCalls = false, bool enableHostIO = false) { - SourceCompilation frontend = SourceCompiler.Compile( + SourceSemanticCompilation frontend = SourceCompiler.CompileSemantic( source, grammarJson, - emitProcessEntryPoint: false, + processImage: processImage, enableModuleCalls: enableModuleCalls, enableHostIO: enableHostIO);