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
+544
View File
@@ -0,0 +1,544 @@
// SPDX-License-Identifier: AGPL-3.0-only
package compiler
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/printer"
"go/scanner"
"go/token"
"path/filepath"
"sort"
"strconv"
"strings"
"unicode/utf8"
)
const headerOpen = "<?sando"
type positionTable struct {
lineStarts []int
size int
}
func newPositionTable(source []byte) positionTable {
starts := []int{0}
for i, b := range source {
if b == '\n' {
starts = append(starts, i+1)
}
}
return positionTable{lineStarts: starts, size: len(source)}
}
func (t positionTable) at(offset int) sourcePosition {
if offset < 0 {
offset = 0
}
if offset > t.size {
offset = t.size
}
lineIndex := sort.Search(len(t.lineStarts), func(i int) bool {
return t.lineStarts[i] > offset
}) - 1
if lineIndex < 0 {
lineIndex = 0
}
return sourcePosition{
Offset: offset,
Line: lineIndex + 1,
Column: offset - t.lineStarts[lineIndex] + 1,
}
}
func parseSource(path string, source []byte) (*sourceFile, []Diagnostic) {
table := newPositionTable(source)
var diagnostics []Diagnostic
if !utf8.Valid(source) {
diagnostics = append(diagnostics, diagnostic(path, table.at(0), "HIM1001", "source is not valid UTF-8"))
return nil, diagnostics
}
if offset := bytes.IndexByte(source, 0); offset >= 0 {
diagnostics = append(diagnostics, diagnostic(path, table.at(offset), "HIM1002", "NUL bytes are not permitted in .sando sources"))
return nil, diagnostics
}
headerStart := 0
if bytes.HasPrefix(source, []byte{0xef, 0xbb, 0xbf}) {
headerStart = 3
}
for headerStart < len(source) && isSpace(source[headerStart]) {
headerStart++
}
if !bytes.HasPrefix(source[headerStart:], []byte(headerOpen)) {
diagnostics = append(diagnostics, diagnostic(path, table.at(headerStart), "HIM1101", "file must begin (after optional UTF-8 BOM and whitespace) with a <?sando go header"))
return nil, diagnostics
}
afterMarker := headerStart + len(headerOpen)
if afterMarker >= len(source) || !isSpace(source[afterMarker]) {
diagnostics = append(diagnostics, diagnostic(path, table.at(afterMarker), "HIM1105", "whitespace is required between <?sando and the target name"))
return nil, diagnostics
}
headerClose := findGoDelimiter(source, afterMarker)
if headerClose < 0 {
diagnostics = append(diagnostics, diagnostic(path, table.at(headerStart), "HIM1102", "unterminated <?sando header"))
return nil, diagnostics
}
directiveBody := source[afterMarker:headerClose]
directiveStart := afterMarker
leading := len(directiveBody) - len(bytes.TrimLeft(directiveBody, " \t\r\n"))
directiveBody = directiveBody[leading:]
directiveStart += leading
if len(directiveBody) < len("go") || string(directiveBody[:2]) != "go" || (len(directiveBody) > 2 && !isSpace(directiveBody[2])) {
diagnostics = append(diagnostics, diagnostic(path, table.at(directiveStart), "HIM1103", "unsupported or missing header target; v1 requires <?sando go"))
return nil, diagnostics
}
declarations := directiveBody[2:]
declarationsStart := directiveStart + 2
declLeading := len(declarations) - len(bytes.TrimLeft(declarations, " \t\r\n"))
declarations = declarations[declLeading:]
declarationsStart += declLeading
if len(declarations) == 0 {
diagnostics = append(diagnostics, diagnostic(path, table.at(declarationsStart), "HIM1104", "header must declare a package and one component function"))
return nil, diagnostics
}
parsedHeader, headerDiagnostics := parseHeader(path, declarations, declarationsStart, table)
diagnostics = append(diagnostics, headerDiagnostics...)
if parsedHeader == nil {
return nil, diagnostics
}
file := &sourceFile{
Path: path,
Mapping: filepath.ToSlash(filepath.Base(path)),
Package: parsedHeader.Package,
Name: parsedHeader.Name,
TypeParams: parsedHeader.TypeParams,
Params: parsedHeader.Params,
Imports: parsedHeader.Imports,
Source: source,
HeaderEnd: headerClose + 2,
AST: parsedHeader.AST,
}
templateDiagnostics := tokenizeTemplate(file, source[headerClose+2:], headerClose+2, table)
diagnostics = append(diagnostics, templateDiagnostics...)
if hasErrors(diagnostics) {
return nil, diagnostics
}
return file, diagnostics
}
func isSpace(b byte) bool {
return b == ' ' || b == '\t' || b == '\r' || b == '\n'
}
type parsedHeader struct {
Package string
Name string
TypeParams string
Params string
Imports []sourceImport
AST *ast.File
}
func parseHeader(path string, declarations []byte, sourceOffset int, table positionTable) (*parsedHeader, []Diagnostic) {
parseInput, syntheticBraceOffset := insertSyntheticFunctionBody(declarations)
fset := token.NewFileSet()
parsed, err := parser.ParseFile(fset, filepath.Base(path), parseInput, parser.AllErrors)
if err != nil {
return nil, parserDiagnostics(path, err, sourceOffset, table, "HIM1110", "invalid Go header")
}
var function *ast.FuncDecl
var imports []sourceImport
var diagnostics []Diagnostic
for _, declaration := range parsed.Decls {
switch declaration := declaration.(type) {
case *ast.GenDecl:
if declaration.Tok != token.IMPORT {
pos := fset.Position(declaration.Pos())
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+pos.Offset), "HIM1111", "header may contain only imports and one component function signature"))
continue
}
for _, spec := range declaration.Specs {
importSpec, ok := spec.(*ast.ImportSpec)
if !ok {
continue
}
importPath, unquoteErr := strconv.Unquote(importSpec.Path.Value)
if unquoteErr != nil || importPath == "" {
pos := fset.Position(importSpec.Path.Pos())
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+pos.Offset), "HIM1112", "invalid import path"))
continue
}
alias := ""
if importSpec.Name != nil {
alias = importSpec.Name.Name
}
if alias == "." {
pos := fset.Position(importSpec.Pos())
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+pos.Offset), "HIM1113", "dot imports are not supported in .sando headers"))
continue
}
imports = append(imports, sourceImport{Alias: alias, Path: importPath})
}
case *ast.FuncDecl:
if function != nil {
pos := fset.Position(declaration.Pos())
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+pos.Offset), "HIM1114", "a .sando file declares exactly one component"))
continue
}
function = declaration
default:
pos := fset.Position(declaration.Pos())
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+pos.Offset), "HIM1111", "unsupported declaration in header"))
}
}
if function == nil {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset), "HIM1115", "header must contain one bodyless component function signature"))
return nil, diagnostics
}
functionPosition := fset.Position(function.Pos())
if function.Name.Name == "init" || parsed.Name.Name == "main" && function.Name.Name == "main" {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+functionPosition.Offset), "HIM1123", fmt.Sprintf("%s.%s is reserved by Go and cannot be a component API", parsed.Name.Name, function.Name.Name)))
}
if function.Recv != nil {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+functionPosition.Offset), "HIM1116", "component functions cannot have receivers"))
}
if function.Type.Results != nil && len(function.Type.Results.List) != 0 {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+functionPosition.Offset), "HIM1117", "component signatures do not declare results; Hime-san generates sando.Component"))
}
if function.Body == nil {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+functionPosition.Offset), "HIM1118", "component signature could not be parsed"))
} else {
bodyOffset := fset.Position(function.Body.Lbrace).Offset
if bodyOffset != syntheticBraceOffset {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+bodyOffset), "HIM1119", "component signature must be bodyless; ?> begins the template body"))
}
}
if hasErrors(diagnostics) {
return nil, diagnostics
}
formattedType, formatErr := formatNode(fset, function.Type)
if formatErr != nil {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+functionPosition.Offset), "HIM1120", "could not format component parameters: "+formatErr.Error()))
return nil, diagnostics
}
typeParams, params, splitErr := splitFormattedFuncType(formattedType)
if splitErr != nil {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+functionPosition.Offset), "HIM1121", "could not recover formatted component signature: "+splitErr.Error()))
return nil, diagnostics
}
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
})
for index := 1; index < len(imports); index++ {
if imports[index-1].Path == imports[index].Path {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset), "HIM1122", fmt.Sprintf("import path %q is declared more than once", imports[index].Path)))
}
}
if hasErrors(diagnostics) {
return nil, diagnostics
}
return &parsedHeader{
Package: parsed.Name.Name,
Name: function.Name.Name,
TypeParams: typeParams,
Params: params,
Imports: imports,
AST: parsed,
}, diagnostics
}
func insertSyntheticFunctionBody(declarations []byte) ([]byte, int) {
trimmed := bytes.TrimRight(declarations, " \t\r\n")
fileSet := token.NewFileSet()
file := fileSet.AddFile("header.go", -1, len(trimmed))
var lexical scanner.Scanner
lexical.Init(file, trimmed, nil, scanner.ScanComments)
seenFunction := false
squareDepth := 0
curlyDepth := 0
parameterDepth := 0
parameterListStarted := false
insertion := -1
for {
position, tok, _ := lexical.Scan()
if tok == token.EOF {
break
}
if !seenFunction {
if tok == token.FUNC {
seenFunction = true
}
continue
}
if parameterListStarted {
switch tok {
case token.LPAREN:
parameterDepth++
case token.RPAREN:
parameterDepth--
if parameterDepth == 0 {
insertion = fileSet.Position(position).Offset + 1
}
}
if insertion >= 0 {
break
}
continue
}
switch tok {
case token.LBRACK:
squareDepth++
case token.RBRACK:
if squareDepth > 0 {
squareDepth--
}
case token.LBRACE:
curlyDepth++
case token.RBRACE:
if curlyDepth > 0 {
curlyDepth--
}
case token.LPAREN:
if squareDepth == 0 && curlyDepth == 0 {
parameterListStarted = true
parameterDepth = 1
}
}
}
if insertion < 0 || insertion > len(trimmed) {
insertion = len(trimmed)
}
parseInput := make([]byte, 0, len(trimmed)+3)
parseInput = append(parseInput, trimmed[:insertion]...)
parseInput = append(parseInput, ' ', '{', '}')
parseInput = append(parseInput, trimmed[insertion:]...)
return parseInput, insertion + 1
}
func splitFormattedFuncType(formatted string) (typeParams, params string, err error) {
remainder := strings.TrimSpace(strings.TrimPrefix(formatted, "func"))
bracketDepth := 0
quote := byte(0)
escaped := false
for index := 0; index < len(remainder); index++ {
b := remainder[index]
if quote != 0 {
if quote != '`' && escaped {
escaped = false
continue
}
if quote != '`' && b == '\\' {
escaped = true
continue
}
if b == quote {
quote = 0
}
continue
}
if b == '"' || b == '\'' || b == '`' {
quote = b
continue
}
switch b {
case '[':
bracketDepth++
case ']':
if bracketDepth > 0 {
bracketDepth--
}
case '(':
if bracketDepth == 0 {
return strings.TrimSpace(remainder[:index]), strings.TrimSpace(remainder[index:]), nil
}
}
}
return "", "", fmt.Errorf("formatted function type has no parameter list")
}
func formatNode(fset *token.FileSet, node any) (string, error) {
var output bytes.Buffer
configuration := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
if err := configuration.Fprint(&output, fset, node); err != nil {
return "", err
}
return output.String(), nil
}
func parserDiagnostics(path string, err error, sourceOffset int, table positionTable, code, prefix string) []Diagnostic {
var diagnostics []Diagnostic
if list, ok := err.(scanner.ErrorList); ok {
for _, parseError := range list {
offset := sourceOffset + parseError.Pos.Offset
diagnostics = append(diagnostics, diagnostic(path, table.at(offset), code, prefix+": "+parseError.Msg))
}
return diagnostics
}
return []Diagnostic{diagnostic(path, table.at(sourceOffset), code, prefix+": "+err.Error())}
}
func tokenizeTemplate(file *sourceFile, template []byte, sourceOffset int, table positionTable) []Diagnostic {
var diagnostics []Diagnostic
cursor := 0
for cursor < len(template) {
openRelative := bytes.Index(template[cursor:], []byte("<?"))
if openRelative < 0 {
if cursor < len(template) {
file.Nodes = append(file.Nodes, rendererNode{Kind: nodeText, Text: string(template[cursor:]), Pos: table.at(sourceOffset + cursor)})
}
break
}
open := cursor + openRelative
if open > cursor {
file.Nodes = append(file.Nodes, rendererNode{Kind: nodeText, Text: string(template[cursor:open]), Pos: table.at(sourceOffset + cursor)})
}
close := -1
if open+2 < len(template) && template[open+2] == '#' {
if closeRelative := bytes.Index(template[open+2:], []byte("?>")); closeRelative >= 0 {
close = open + 2 + closeRelative
}
} else {
close = findGoDelimiter(template, open+2)
}
if close < 0 {
diagnostics = append(diagnostics, diagnostic(file.Path, table.at(sourceOffset+open), "HIM1201", "unterminated template tag"))
break
}
kind := nodeStatement
contentStart := open + 2
if contentStart < close {
switch template[contentStart] {
case '=':
kind = nodeExpression
contentStart++
case '~':
kind = nodeComponent
contentStart++
case '#':
kind = nodeComment
contentStart++
}
}
content := template[contentStart:close]
trimmed := bytes.TrimSpace(content)
trimLeading := len(content) - len(bytes.TrimLeft(content, " \t\r\n"))
position := table.at(sourceOffset + contentStart + trimLeading)
if bytes.HasPrefix(bytes.TrimSpace(template[open+2:close]), []byte("sando")) {
diagnostics = append(diagnostics, diagnostic(file.Path, table.at(sourceOffset+open), "HIM1202", "<?sando is only valid as the file header"))
} else if kind != nodeComment && len(trimmed) == 0 {
diagnostics = append(diagnostics, diagnostic(file.Path, table.at(sourceOffset+open), "HIM1203", "empty template tag"))
} else {
node := rendererNode{Kind: kind, Text: string(trimmed), Context: ContextNone, Pos: position}
if kind == nodeExpression || kind == nodeComponent {
if _, err := parser.ParseExprFrom(token.NewFileSet(), filepath.Base(file.Path), trimmed, parser.AllErrors); err != nil {
diagnostics = append(diagnostics, expressionDiagnostics(file.Path, err, position, table, sourceOffset+contentStart+trimLeading)...)
} else {
file.Nodes = append(file.Nodes, node)
}
} else {
file.Nodes = append(file.Nodes, node)
}
}
cursor = close + 2
}
return diagnostics
}
// findGoDelimiter returns the first ?> outside Go strings, rune literals, raw
// strings, and comments. Statement tags may be structurally incomplete across
// template regions, so requiring each region to parse independently would
// reject ordinary `if { ?>...<? }` usage.
func findGoDelimiter(source []byte, start int) int {
type lexicalState uint8
const (
lexicalNormal lexicalState = iota
lexicalString
lexicalRune
lexicalRawString
lexicalLineComment
lexicalBlockComment
)
state := lexicalNormal
escaped := false
for index := start; index < len(source); index++ {
b := source[index]
next := byte(0)
if index+1 < len(source) {
next = source[index+1]
}
switch state {
case lexicalNormal:
switch {
case b == '?' && next == '>':
return index
case b == '"':
state = lexicalString
escaped = false
case b == '\'':
state = lexicalRune
escaped = false
case b == '`':
state = lexicalRawString
case b == '/' && next == '/':
state = lexicalLineComment
index++
case b == '/' && next == '*':
state = lexicalBlockComment
index++
}
case lexicalString, lexicalRune:
if escaped {
escaped = false
continue
}
if b == '\\' {
escaped = true
continue
}
if (state == lexicalString && b == '"') || (state == lexicalRune && b == '\'') {
state = lexicalNormal
}
case lexicalRawString:
if b == '`' {
state = lexicalNormal
}
case lexicalLineComment:
if b == '\n' {
state = lexicalNormal
}
case lexicalBlockComment:
if b == '*' && next == '/' {
state = lexicalNormal
index++
}
}
}
return -1
}
func expressionDiagnostics(path string, err error, fallback sourcePosition, table positionTable, sourceOffset int) []Diagnostic {
if list, ok := err.(scanner.ErrorList); ok {
diagnostics := make([]Diagnostic, 0, len(list))
for _, parseError := range list {
diagnostics = append(diagnostics, diagnostic(path, table.at(sourceOffset+parseError.Pos.Offset), "HIM1210", "invalid Go expression: "+parseError.Msg))
}
return diagnostics
}
return []Diagnostic{diagnostic(path, fallback, "HIM1210", "invalid Go expression: "+strings.TrimSpace(err.Error()))}
}