Files
sandwich-hime/internal/compiler/operation.go
T
gamertan b7a84054d7 release: prepare Sandwich Hime v1 beta
Publish the sanitized Beta 1 source candidate with version-stable generated provenance, classroom/evaluation support boundaries, provisional macOS support, signed-source release policy, and an exact candidate preflight.

Material implementation, drafting, and review were assisted by OpenAI Codex. Cole Speelman reviewed the public snapshot and accepts human responsibility for the contribution.

Signed-off-by: Cole Speelman <crspeelman@gmail.com>
2026-08-12 14:37:53 -04:00

375 lines
13 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"bytes"
"context"
"fmt"
"go/ast"
"go/parser"
"os"
"path/filepath"
"sort"
)
// Generate compiles all discovered .sando files in memory, then atomically
// replaces only changed, Hime-san-owned .sando.go outputs. Any parse, context,
// format, cycle, or ownership error prevents every output write.
func Generate(ctx context.Context, paths []string) (Result, error) {
compiled, result := compileOperation(ctx, paths)
if hasErrors(result.Diagnostics) {
return result, errorFromDiagnostics(result.Diagnostics)
}
// Validate every destination before performing the first mutation.
for _, file := range compiled {
info, err := os.Lstat(file.OutputPath)
if err != nil {
if os.IsNotExist(err) {
continue
}
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2101", "cannot inspect generated output: "+err.Error()))
continue
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2102", "refusing to replace a non-regular or symlink output"))
continue
}
existing, readErr := os.ReadFile(file.OutputPath)
if readErr != nil {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2103", "cannot read generated output: "+readErr.Error()))
continue
}
if !bytes.HasPrefix(existing, []byte(generatedPrefix+"\n")) {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2104", "refusing to overwrite a file not owned by Hime-san"))
}
}
sortDiagnostics(result.Diagnostics)
if hasErrors(result.Diagnostics) {
return result, errorFromDiagnostics(result.Diagnostics)
}
for index, file := range compiled {
if err := ctx.Err(); err != nil {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.SourcePath, sourcePosition{Line: 1, Column: 1}, "HIM2001", "operation canceled: "+err.Error()))
break
}
existing, readErr := os.ReadFile(file.OutputPath)
if readErr == nil && generatedCodeEqual(existing, file.Code) {
result.Files[index].Changed = false
result.Unchanged++
continue
}
// Generated Go can contain every literal present in its source. A new
// output therefore must not be more permissive than the source file.
// Execute bits are never meaningful for Go source and are stripped.
mode := os.FileMode(file.sourceMode) & 0o666
if info, statErr := os.Stat(file.OutputPath); statErr == nil {
mode = info.Mode().Perm()
}
if writeErr := atomicWrite(file.OutputPath, file.Code, mode); writeErr != nil {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2110", "atomic output replacement failed: "+writeErr.Error()))
break
}
result.Files[index].Changed = true
result.Changed++
}
sortDiagnostics(result.Diagnostics)
return result, errorFromDiagnostics(result.Diagnostics)
}
// Check validates sources and reports missing or stale generated output without
// writing to the filesystem. Warnings, including trusted-value audit findings,
// do not cause Check to fail by themselves.
func Check(ctx context.Context, paths []string) (Result, error) {
compiled, result := compileOperation(ctx, paths)
if hasErrors(result.Diagnostics) {
return result, errorFromDiagnostics(result.Diagnostics)
}
for index, file := range compiled {
if err := ctx.Err(); err != nil {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.SourcePath, sourcePosition{Line: 1, Column: 1}, "HIM2001", "operation canceled: "+err.Error()))
break
}
info, lstatErr := os.Lstat(file.OutputPath)
if lstatErr == nil && (info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular()) {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2205", "generated output is a symlink or non-regular file"))
continue
}
if lstatErr != nil && !os.IsNotExist(lstatErr) {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2202", "cannot inspect generated output: "+lstatErr.Error()))
continue
}
existing, err := os.ReadFile(file.OutputPath)
if err != nil {
if os.IsNotExist(err) {
result.Files[index].Missing = true
result.Missing++
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2201", "generated output is missing; run himesan generate"))
} else {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2202", "cannot read generated output: "+err.Error()))
}
continue
}
if generatedCodeEqual(existing, file.Code) {
result.Unchanged++
continue
}
result.Files[index].Stale = true
result.Stale++
if !bytes.HasPrefix(existing, []byte(generatedPrefix+"\n")) {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2203", "expected output exists but is not owned by Hime-san"))
} else {
result.Diagnostics = append(result.Diagnostics, diagnostic(file.OutputPath, sourcePosition{Line: 1, Column: 1}, "HIM2204", "generated output is stale; run himesan generate"))
}
}
sortDiagnostics(result.Diagnostics)
return result, errorFromDiagnostics(result.Diagnostics)
}
func compileOperation(ctx context.Context, paths []string) ([]CompiledFile, Result) {
discovered, discoveryDiagnostics := discover(ctx, paths)
result := Result{Discovered: len(discovered), Diagnostics: discoveryDiagnostics}
compiled := make([]CompiledFile, 0, len(discovered))
for _, sourcePath := range discovered {
if err := ctx.Err(); err != nil {
result.Diagnostics = append(result.Diagnostics, diagnostic(sourcePath, sourcePosition{Line: 1, Column: 1}, "HIM2001", "operation canceled: "+err.Error()))
break
}
info, lstatErr := os.Lstat(sourcePath)
if lstatErr != nil {
result.Diagnostics = append(result.Diagnostics, diagnostic(sourcePath, sourcePosition{Line: 1, Column: 1}, "HIM2010", "cannot inspect source before compilation: "+lstatErr.Error()))
continue
}
if info.Mode()&os.ModeSymlink != 0 || !info.Mode().IsRegular() {
result.Diagnostics = append(result.Diagnostics, diagnostic(sourcePath, sourcePosition{Line: 1, Column: 1}, "HIM2012", "source changed into a symlink or non-regular file during discovery"))
continue
}
source, err := os.ReadFile(sourcePath)
if err != nil {
result.Diagnostics = append(result.Diagnostics, diagnostic(sourcePath, sourcePosition{Line: 1, Column: 1}, "HIM2010", "cannot read source: "+err.Error()))
continue
}
output, diagnostics := compileWithMapping(sourcePath, source, moduleRelativeSourcePath(sourcePath))
result.Diagnostics = append(result.Diagnostics, diagnostics...)
if output.Code != nil {
output.sourceMode = uint32(info.Mode().Perm())
compiled = append(compiled, output)
result.Files = append(result.Files, FileResult{SourcePath: output.SourcePath, OutputPath: output.OutputPath})
}
}
result.Diagnostics = append(result.Diagnostics, detectComponentCycles(compiled)...)
sort.SliceStable(compiled, func(i, j int) bool { return compiled[i].SourcePath < compiled[j].SourcePath })
sort.SliceStable(result.Files, func(i, j int) bool { return result.Files[i].SourcePath < result.Files[j].SourcePath })
sortDiagnostics(result.Diagnostics)
return compiled, result
}
func moduleRelativeSourcePath(sourcePath string) string {
absolute, err := filepath.Abs(sourcePath)
if err != nil {
return filepath.ToSlash(filepath.Base(sourcePath))
}
directory := filepath.Dir(absolute)
for {
modulePath := filepath.Join(directory, "go.mod")
if info, statErr := os.Lstat(modulePath); statErr == nil && info.Mode().IsRegular() {
if relative, relativeErr := filepath.Rel(directory, absolute); relativeErr == nil {
return filepath.ToSlash(relative)
}
}
parent := filepath.Dir(directory)
if parent == directory {
break
}
directory = parent
}
return filepath.ToSlash(filepath.Base(sourcePath))
}
func atomicWrite(path string, content []byte, mode os.FileMode) (returnErr error) {
directory := filepath.Dir(path)
temporary, err := os.CreateTemp(directory, ".himesan-*.tmp")
if err != nil {
return err
}
temporaryPath := temporary.Name()
closed := false
defer func() {
var closeErr error
if !closed {
closeErr = temporary.Close()
}
removeErr := os.Remove(temporaryPath)
if returnErr == nil && closeErr != nil {
returnErr = closeErr
}
if returnErr == nil && removeErr != nil && !os.IsNotExist(removeErr) {
returnErr = removeErr
}
}()
if _, err := temporary.Write(content); err != nil {
return err
}
if err := temporary.Chmod(mode.Perm()); err != nil {
return err
}
if err := temporary.Sync(); err != nil {
return err
}
if err := temporary.Close(); err != nil {
return err
}
closed = true
if err := replaceFile(temporaryPath, path); err != nil {
return err
}
if directoryHandle, err := os.Open(directory); err == nil {
_ = directoryHandle.Sync()
_ = directoryHandle.Close()
}
return nil
}
type componentKey struct {
directory string
packageID string
name string
}
type componentEdge struct {
target componentKey
position sourcePosition
}
func detectComponentCycles(files []CompiledFile) []Diagnostic {
byKey := make(map[componentKey]CompiledFile, len(files))
var diagnostics []Diagnostic
for _, file := range files {
key := componentKey{directory: filepath.Clean(filepath.Dir(file.SourcePath)), packageID: file.Package, name: file.Component}
if previous, exists := byKey[key]; exists {
diagnostics = append(diagnostics,
diagnostic(previous.SourcePath, sourcePosition{Line: 1, Column: 1}, "HIM1500", fmt.Sprintf("component %s is also declared by %s", file.Component, file.SourcePath)),
diagnostic(file.SourcePath, sourcePosition{Line: 1, Column: 1}, "HIM1500", fmt.Sprintf("component %s is also declared by %s", file.Component, previous.SourcePath)),
)
continue
}
byKey[key] = file
}
edges := make(map[componentKey][]componentEdge)
for key, file := range byKey {
if file.source == nil {
continue
}
for _, node := range file.source.Nodes {
if node.Kind != nodeComponent {
continue
}
expression, err := parser.ParseExpr(node.Text)
if err != nil {
continue
}
calledName := rootCalledIdentifier(expression)
if calledName == "" {
continue
}
target := componentKey{directory: key.directory, packageID: key.packageID, name: calledName}
if _, exists := byKey[target]; exists {
edges[key] = append(edges[key], componentEdge{target: target, position: node.Pos})
}
}
sort.SliceStable(edges[key], func(i, j int) bool { return edges[key][i].target.name < edges[key][j].target.name })
}
const (
unvisited = iota
visiting
visited
)
state := make(map[componentKey]int)
stack := make([]componentKey, 0)
reported := make(map[componentKey]bool)
var visit func(componentKey)
visit = func(key componentKey) {
state[key] = visiting
stack = append(stack, key)
for _, edge := range edges[key] {
target := edge.target
if state[target] == unvisited {
visit(target)
continue
}
if state[target] != visiting {
continue
}
cycleStart := 0
for cycleStart < len(stack) && stack[cycleStart] != target {
cycleStart++
}
cycle := append(append([]componentKey(nil), stack[cycleStart:]...), target)
names := make([]string, 0, len(cycle))
for _, member := range cycle {
names = append(names, member.name)
}
for memberIndex, member := range cycle[:len(cycle)-1] {
if reported[member] {
continue
}
reported[member] = true
file := byKey[member]
position := sourcePosition{Line: 1, Column: 1}
next := cycle[memberIndex+1]
for _, memberEdge := range edges[member] {
if memberEdge.target == next {
position = memberEdge.position
break
}
}
diagnostics = append(diagnostics, diagnostic(file.SourcePath, position, "HIM1501", "static component cycle detected: "+fmt.Sprint(names)))
}
}
stack = stack[:len(stack)-1]
state[key] = visited
}
keys := make([]componentKey, 0, len(byKey))
for key := range byKey {
keys = append(keys, key)
}
sort.SliceStable(keys, func(i, j int) bool {
if keys[i].directory != keys[j].directory {
return keys[i].directory < keys[j].directory
}
if keys[i].packageID != keys[j].packageID {
return keys[i].packageID < keys[j].packageID
}
return keys[i].name < keys[j].name
})
for _, key := range keys {
if state[key] == unvisited {
visit(key)
}
}
sortDiagnostics(diagnostics)
return diagnostics
}
func rootCalledIdentifier(expression ast.Expr) string {
for {
switch typed := expression.(type) {
case *ast.ParenExpr:
expression = typed.X
case *ast.CallExpr:
expression = typed.Fun
case *ast.IndexExpr:
expression = typed.X
case *ast.IndexListExpr:
expression = typed.X
case *ast.Ident:
return typed.Name
default:
return ""
}
}
}