feat: some basic commands

This commit is contained in:
2026-02-05 20:41:35 -05:00
parent 22e8a99362
commit 7750d8615f
8 changed files with 274 additions and 54 deletions

View File

@@ -1,55 +1,94 @@
package main
import (
"fmt"
"os"
"git.maximhutz.com/max/lambda/internal/cli"
"git.maximhutz.com/max/lambda/internal/config"
"github.com/spf13/cobra"
)
func main() {
// Parse CLI arguments.
options, err := config.FromArgs()
cli.HandleError(err)
func Lambda() *cobra.Command {
cmd := &cobra.Command{
Use: "lambda",
Short: "Lambda calculus interpreter",
Long: "A lambda calculus interpreter supporting multiple representations.",
RunE: func(cmd *cobra.Command, args []string) error {
// Legacy behavior when no subcommand is given.
options, err := config.FromArgs()
if err != nil {
return err
}
logger := options.GetLogger()
logger.Info("using program arguments", "args", os.Args)
logger.Info("parsed CLI options", "options", options)
logger := options.GetLogger()
logger.Info("using program arguments", "args", os.Args)
logger.Info("parsed CLI options", "options", options)
r := GetRegistry()
r := GetRegistry()
// Get input.
input, err := options.Source.Extract()
cli.HandleError(err)
// Get input.
input, err := options.Source.Extract()
if err != nil {
return err
}
// Parse code into syntax tree.
repr, err := r.Unmarshal(input, "saccharine")
cli.HandleError(err)
logger.Info("parsed syntax tree", "tree", repr)
// Parse code into syntax tree.
repr, err := r.Unmarshal(input, "saccharine")
if err != nil {
return err
}
logger.Info("parsed syntax tree", "tree", repr)
// Compile expression to lambda calculus.
compiled, err := r.ConvertTo(repr, "lambda")
cli.HandleError(err)
logger.Info("compiled λ expression", "tree", compiled)
// Compile expression to lambda calculus.
compiled, err := r.ConvertTo(repr, "lambda")
if err != nil {
return err
}
logger.Info("compiled λ expression", "tree", compiled)
// Create reducer with the compiled expression.
engine, err := r.GetDefaultEngine("lambda")
cli.HandleError(err)
// Create reducer with the compiled expression.
engine, err := r.GetDefaultEngine("lambda")
if err != nil {
return err
}
process := engine.Load()
err = process.Set(compiled)
cli.HandleError(err)
// Run reduction.
for process.Step(1) {
process := engine.Load()
err = process.Set(compiled)
if err != nil {
return err
}
// Run reduction.
for process.Step(1) {
}
// Return the final reduced result.
result, err := process.Get()
if err != nil {
return err
}
output, err := r.Marshal(result)
if err != nil {
return err
}
return options.Destination.Write(output)
},
}
// Return the final reduced result.
result, err := process.Get()
cli.HandleError(err)
cmd.PersistentFlags().BoolP("verbose", "v", false, "Enable verbose output")
output, err := r.Marshal(result)
cli.HandleError(err)
cmd.AddCommand(LambdaConvert())
cmd.AddCommand(LambdaEngine())
err = options.Destination.Write(output)
cli.HandleError(err)
return cmd
}
func main() {
lambda := Lambda()
if err := lambda.Execute(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}

View File

@@ -0,0 +1,100 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"git.maximhutz.com/max/lambda/internal/cli"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// inferReprFromPath returns the repr type based on file extension.
func inferReprFromPath(path string) (string, error) {
switch ext := strings.ToLower(filepath.Ext(path)); ext {
case ".lam", ".lambda":
return "lambda", nil
case ".sac", ".saccharine":
return "saccharine", nil
default:
return "", fmt.Errorf("unknown file extension '%s'", ext)
}
}
func LambdaConvert() *cobra.Command {
cmd := &cobra.Command{
Use: "convert <input> <output>",
Short: "Convert between lambda calculus representations",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
inputPath := args[0]
outputPath := args[1]
// Infer repr types from extensions.
inputRepr, err := inferReprFromPath(inputPath)
if err != nil {
return fmt.Errorf("input file: %w", err)
}
outputRepr, err := inferReprFromPath(outputPath)
if err != nil {
return fmt.Errorf("output file: %w", err)
}
// Read input file.
input, err := os.ReadFile(inputPath)
if err != nil {
return fmt.Errorf("reading input file: %w", err)
}
r := GetRegistry()
// Parse input into syntax tree.
repr, err := r.Unmarshal(string(input), inputRepr)
if err != nil {
return fmt.Errorf("parsing input: %w", err)
}
if viper.GetBool("verbose") {
fmt.Fprintf(os.Stderr, "Parsed %s from %s\n", inputRepr, inputPath)
}
// Convert to output repr if different.
var result cli.Repr
if inputRepr != outputRepr {
result, err = r.ConvertTo(repr, outputRepr)
if err != nil {
return fmt.Errorf("converting %s to %s: %w", inputRepr, outputRepr, err)
}
if viper.GetBool("verbose") {
fmt.Fprintf(os.Stderr, "Converted to %s\n", outputRepr)
}
} else {
result = repr
}
// Marshal output.
output, err := r.Marshal(result)
if err != nil {
return fmt.Errorf("marshaling output: %w", err)
}
// Write output file.
err = os.WriteFile(outputPath, []byte(output), 0644)
if err != nil {
return fmt.Errorf("writing output file: %w", err)
}
if viper.GetBool("verbose") {
fmt.Fprintf(os.Stderr, "Wrote %s to %s\n", outputRepr, outputPath)
}
return nil
},
}
return cmd
}

View File

@@ -0,0 +1,19 @@
package main
import (
"github.com/spf13/cobra"
)
func LambdaEngine() *cobra.Command {
cmd := &cobra.Command{
Use: "engine",
Short: "Information about available engines",
RunE: func(cmd *cobra.Command, args []string) error {
return cmd.Help()
},
}
cmd.AddCommand(LambdaEngineList())
return cmd
}

View File

@@ -0,0 +1,26 @@
package main
import (
"fmt"
"github.com/spf13/cobra"
)
func LambdaEngineList() *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Aliases: []string{"ls"},
Short: "List available engines",
RunE: func(cmd *cobra.Command, args []string) error {
r := GetRegistry()
for engine := range r.ListEngines() {
fmt.Println(engine.Name())
}
return nil
},
}
return cmd
}