feat: publish the Sandwich Hime source preview

Signed-off-by: Cole Speelman <gamertan@noreply.localhost>
This commit is contained in:
2026-08-11 20:15:06 -04:00
commit 9b29b3d7f8
100 changed files with 10989 additions and 0 deletions
+318
View File
@@ -0,0 +1,318 @@
// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"bytes"
"crypto/sha256"
"fmt"
"go/format"
"go/scanner"
"go/token"
"path/filepath"
"sort"
"strconv"
"strings"
"gamertan.com/sandwich-hime/internal/version"
)
type backendImports struct {
Context string
IO string
Sando string
All []sourceImport
}
// Compile parses, context-checks, and formats one .sando source entirely in
// memory. It never reads project metadata, writes a file, or executes Go code.
func Compile(path string, source []byte) (CompiledFile, []Diagnostic) {
return compileWithMapping(path, source, filepath.ToSlash(filepath.Base(path)))
}
func compileWithMapping(path string, source []byte, mapping string) (CompiledFile, []Diagnostic) {
cleanPath := filepath.Clean(path)
file, diagnostics := parseSource(cleanPath, source)
if file == nil {
sortDiagnostics(diagnostics)
return CompiledFile{}, diagnostics
}
file.Mapping = mapping
diagnostics = append(diagnostics, analyzeContexts(file)...)
diagnostics = append(diagnostics, auditTrustCalls(file)...)
if hasErrors(diagnostics) {
sortDiagnostics(diagnostics)
return CompiledFile{}, diagnostics
}
code, backendDiagnostics := generateGo(file)
diagnostics = append(diagnostics, backendDiagnostics...)
if hasErrors(diagnostics) {
sortDiagnostics(diagnostics)
return CompiledFile{}, diagnostics
}
digest := fmt.Sprintf("%x", sha256.Sum256(source))
compiled := CompiledFile{
SourcePath: cleanPath,
OutputPath: cleanPath + ".go",
Package: file.Package,
Component: file.Name,
Digest: digest,
Code: code,
source: file,
}
sortDiagnostics(diagnostics)
return compiled, diagnostics
}
func generateGo(file *sourceFile) ([]byte, []Diagnostic) {
imports, diagnostics := prepareImports(file)
if hasErrors(diagnostics) {
return nil, diagnostics
}
usedIdentifiers := sourceIdentifiers(file.Source)
contextName := uniqueIdentifier("__himesan_render_context", usedIdentifiers)
writerName := uniqueIdentifier("__himesan_writer", usedIdentifiers)
errorName := uniqueIdentifier("__himesan_error", usedIdentifiers)
digest := fmt.Sprintf("%x", sha256.Sum256(file.Source))
var output strings.Builder
output.WriteString(generatedPrefix)
output.WriteByte('\n')
fmt.Fprintf(&output, "// himesan:compiler %s\n", version.Compiler)
fmt.Fprintf(&output, "// himesan:runtime-abi %s\n", version.RuntimeABI)
fmt.Fprintf(&output, "// himesan:source-sha256 %s\n\n", digest)
fmt.Fprintf(&output, "package %s\n\n", file.Package)
output.WriteString("import (\n")
for _, imported := range imports.All {
if imported.Alias != "" {
fmt.Fprintf(&output, "\t%s %q\n", imported.Alias, imported.Path)
} else {
fmt.Fprintf(&output, "\t%q\n", imported.Path)
}
}
output.WriteString(")\n\n")
fmt.Fprintf(&output, "var _ = %s.ABI\n\n", imports.Sando)
fmt.Fprintf(&output, "func %s%s%s %s.Component {\n", file.Name, file.TypeParams, file.Params, imports.Sando)
fmt.Fprintf(&output, "\treturn %s.ComponentFunc(func(%s %s.Context, %s %s.Writer) error {\n", imports.Sando, contextName, imports.Context, writerName, imports.IO)
fmt.Fprintf(&output, "\t\t_ = %s\n", contextName)
directiveName := sanitizeDirectivePath(file.Mapping)
for _, node := range file.Nodes {
if node.Kind == nodeComment {
continue
}
fmt.Fprintf(&output, "//line %s:%d:%d\n", directiveName, node.Pos.Line, node.Pos.Column)
switch node.Kind {
case nodeText:
if node.Text == "" {
continue
}
fmt.Fprintf(&output, "if %s := %s.WriteString(%s, %s); %s != nil { return %s }\n", errorName, imports.Sando, writerName, strconv.Quote(node.Text), errorName, errorName)
case nodeStatement:
output.WriteString(node.Text)
output.WriteByte('\n')
case nodeExpression:
helper := "WriteText"
switch node.Context {
case ContextAttr:
helper = "WriteAttr"
case ContextRCDATA:
helper = "WriteRCDATA"
case ContextURL:
helper = "WriteURL"
case ContextJS:
helper = "WriteJS"
case ContextCSS:
helper = "WriteCSS"
}
fmt.Fprintf(&output, "if %s := %s.%s(%s, (%s)); %s != nil { return %s }\n", errorName, imports.Sando, helper, writerName, node.Text, errorName, errorName)
case nodeComponent:
fmt.Fprintf(&output, "if %s := %s.Render(%s, %s, (%s)); %s != nil { return %s }\n", errorName, imports.Sando, contextName, writerName, node.Text, errorName, errorName)
}
}
output.WriteString("return nil\n")
output.WriteString("})\n")
output.WriteString("}\n")
formatted, err := format.Source([]byte(output.String()))
if err != nil {
position := sourcePosition{Line: 1, Column: 1}
message := err.Error()
if list, ok := err.(scanner.ErrorList); ok && len(list) != 0 {
position.Line = list[0].Pos.Line
position.Column = list[0].Pos.Column
message = list[0].Msg
}
return nil, []Diagnostic{diagnostic(file.Path, position, "HIM1401", "generated Go is invalid: "+message)}
}
return formatted, diagnostics
}
func sanitizeDirectivePath(path string) string {
path = filepath.ToSlash(path)
var sanitized strings.Builder
for index := 0; index < len(path); index++ {
b := path[index]
if b < 0x20 || b == 0x7f || b == '%' {
fmt.Fprintf(&sanitized, "%%%02X", b)
continue
}
sanitized.WriteByte(b)
}
if sanitized.Len() == 0 {
return "source.sando"
}
return sanitized.String()
}
func prepareImports(file *sourceFile) (backendImports, []Diagnostic) {
imports := append([]sourceImport(nil), file.Imports...)
identifiers := sourceIdentifiers(file.Source)
for _, imported := range imports {
if imported.Alias != "" && imported.Alias != "_" {
identifiers[imported.Alias] = true
} else if imported.Alias == "" {
identifiers[defaultImportName(imported.Path)] = true
}
}
var diagnostics []Diagnostic
ensure := func(path, base string) string {
for _, imported := range imports {
if imported.Path != path {
continue
}
if imported.Alias == "_" {
diagnostics = append(diagnostics, diagnostic(file.Path, sourcePosition{Line: 1, Column: 1}, "HIM1410", fmt.Sprintf("internal dependency %q cannot be imported for side effects", path)))
return ""
}
if imported.Alias != "" {
return imported.Alias
}
return defaultImportName(path)
}
alias := uniqueIdentifier(base, identifiers)
imports = append(imports, sourceImport{Alias: alias, Path: path})
return alias
}
contextAlias := ensure("context", "__himesan_context")
ioAlias := ensure("io", "__himesan_io")
sandoAlias := ensure(runtimeImportPath, "__himesan_sando")
sort.SliceStable(imports, func(i, j int) bool {
if imports[i].Path != imports[j].Path {
return imports[i].Path < imports[j].Path
}
return imports[i].Alias < imports[j].Alias
})
return backendImports{Context: contextAlias, IO: ioAlias, Sando: sandoAlias, All: imports}, diagnostics
}
func sourceIdentifiers(source []byte) map[string]bool {
identifiers := make(map[string]bool)
var lexical scanner.Scanner
fileSet := token.NewFileSet()
file := fileSet.AddFile("source.sando", -1, len(source))
lexical.Init(file, source, nil, scanner.ScanComments)
for {
_, tok, literal := lexical.Scan()
if tok == token.EOF {
break
}
if tok == token.IDENT {
identifiers[literal] = true
}
}
return identifiers
}
func uniqueIdentifier(base string, used map[string]bool) string {
name := base
for suffix := 2; used[name]; suffix++ {
name = fmt.Sprintf("%s_%d", base, suffix)
}
used[name] = true
return name
}
func defaultImportName(path string) string {
base := filepath.Base(path)
if index := strings.IndexByte(base, '.'); index >= 0 {
base = base[:index]
}
base = strings.ReplaceAll(base, "-", "_")
return base
}
func auditTrustCalls(file *sourceFile) []Diagnostic {
trusted := map[string]bool{
"TrustHTML": true,
"TrustURL": true,
"TrustJS": true,
"TrustCSS": true,
}
var diagnostics []Diagnostic
trustedTypeSeen := make(map[string]bool)
var headerScanner scanner.Scanner
headerFileSet := token.NewFileSet()
headerFile := headerFileSet.AddFile(filepath.Base(file.Path), -1, file.HeaderEnd)
headerScanner.Init(headerFile, file.Source[:file.HeaderEnd], nil, scanner.ScanComments)
for {
_, tok, literal := headerScanner.Scan()
if tok == token.EOF {
break
}
if tok == token.IDENT && (literal == "TrustedHTML" || literal == "TrustedURL" || literal == "TrustedJS" || literal == "TrustedCSS") && !trustedTypeSeen[literal] {
trustedTypeSeen[literal] = true
diagnostics = append(diagnostics, Diagnostic{
Path: file.Path,
Line: 1,
Column: 1,
Code: "HIM1903",
Severity: SeverityWarning,
Message: fmt.Sprintf("component signature names %s; audit every value supplied through this trust boundary", literal),
})
}
}
for _, node := range file.Nodes {
if node.Kind != nodeExpression && node.Kind != nodeComponent && node.Kind != nodeStatement {
continue
}
if node.Kind == nodeExpression && (node.Context == ContextJS || node.Context == ContextCSS) {
diagnostics = append(diagnostics, Diagnostic{
Path: file.Path,
Line: node.Pos.Line,
Column: node.Pos.Column,
Code: "HIM1902",
Severity: SeverityWarning,
Message: fmt.Sprintf("dynamic %s output requires an explicitly trusted runtime value; audit its provenance", node.Context),
})
}
var lexical scanner.Scanner
set := token.NewFileSet()
goFile := set.AddFile(filepath.Base(file.Path), -1, len(node.Text))
lexical.Init(goFile, []byte(node.Text), nil, scanner.ScanComments)
for {
_, tok, literal := lexical.Scan()
if tok == token.EOF {
break
}
if tok == token.IDENT && trusted[literal] {
diagnostics = append(diagnostics, Diagnostic{
Path: file.Path,
Line: node.Pos.Line,
Column: node.Pos.Column,
Code: "HIM1901",
Severity: SeverityWarning,
Message: fmt.Sprintf("conspicuous trusted-value constructor %s is used; audit its provenance", literal),
})
}
}
}
return diagnostics
}
func bytesEqual(a, b []byte) bool {
return bytes.Equal(a, b)
}