Compare commits
7 Commits
1d981ecce3
...
412d3924eb
| Author | SHA1 | Date | |
|---|---|---|---|
|
412d3924eb
|
|||
|
05cd8bc4f3
|
|||
|
aabe92f2dc
|
|||
|
13989e4c61
|
|||
|
529abb7c26
|
|||
|
351faa7e08
|
|||
|
3f9f3a603f
|
@@ -7,3 +7,8 @@ Making a lambda calculus interpreter in Go.
|
|||||||
- Exhaustive sum types.
|
- Exhaustive sum types.
|
||||||
- Recursive descent and left-recursion.
|
- Recursive descent and left-recursion.
|
||||||
- Observer pattern, event emission.
|
- Observer pattern, event emission.
|
||||||
|
|
||||||
|
## Links
|
||||||
|
|
||||||
|
<https://zicklag.katharos.group/blog/interaction-nets-combinators-calculus/>
|
||||||
|
<https://arxiv.org/pdf/2505.20314>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import (
|
|||||||
|
|
||||||
"git.maximhutz.com/max/lambda/internal/cli"
|
"git.maximhutz.com/max/lambda/internal/cli"
|
||||||
"git.maximhutz.com/max/lambda/internal/config"
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
"git.maximhutz.com/max/lambda/internal/executer"
|
"git.maximhutz.com/max/lambda/internal/engine"
|
||||||
"git.maximhutz.com/max/lambda/internal/explanation"
|
"git.maximhutz.com/max/lambda/internal/explanation"
|
||||||
"git.maximhutz.com/max/lambda/internal/performance"
|
"git.maximhutz.com/max/lambda/internal/performance"
|
||||||
"git.maximhutz.com/max/lambda/internal/statistics"
|
"git.maximhutz.com/max/lambda/internal/statistics"
|
||||||
@@ -15,8 +15,8 @@ import (
|
|||||||
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
"git.maximhutz.com/max/lambda/pkg/saccharine"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Run main application.
|
|
||||||
func main() {
|
func main() {
|
||||||
|
// Parse CLI arguments.
|
||||||
options, err := config.FromArgs()
|
options, err := config.FromArgs()
|
||||||
cli.HandleError(err)
|
cli.HandleError(err)
|
||||||
|
|
||||||
@@ -24,7 +24,8 @@ func main() {
|
|||||||
logger.Info("using program arguments", "args", os.Args)
|
logger.Info("using program arguments", "args", os.Args)
|
||||||
logger.Info("parsed CLI options", "options", options)
|
logger.Info("parsed CLI options", "options", options)
|
||||||
|
|
||||||
input, err := options.Source.Pull()
|
// Get input.
|
||||||
|
input, err := options.Source.Extract()
|
||||||
cli.HandleError(err)
|
cli.HandleError(err)
|
||||||
|
|
||||||
// Parse tokens.
|
// Parse tokens.
|
||||||
@@ -35,33 +36,46 @@ func main() {
|
|||||||
// Turn tokens into syntax tree.
|
// Turn tokens into syntax tree.
|
||||||
expression, err := saccharine.Parse(tokens)
|
expression, err := saccharine.Parse(tokens)
|
||||||
cli.HandleError(err)
|
cli.HandleError(err)
|
||||||
if options.Verbose {
|
logger.Info("parsed syntax tree", "tree", saccharine.Stringify(expression))
|
||||||
logger.Info("parsed syntax tree", "tree", saccharine.Stringify(expression))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Compile expression to lambda calculus.
|
||||||
compiled := convert.SaccharineToLambda(expression)
|
compiled := convert.SaccharineToLambda(expression)
|
||||||
if options.Verbose {
|
logger.Info("compiled lambda expression", "tree", lambda.Stringify(compiled))
|
||||||
logger.Info("compiled lambda expression", "tree", lambda.Stringify(compiled))
|
|
||||||
}
|
|
||||||
|
|
||||||
process := executer.New(options, &compiled)
|
// Create reduction engine.
|
||||||
|
process := engine.New(options, &compiled)
|
||||||
|
|
||||||
|
// If the user selected to track CPU performance, attach a profiler to the
|
||||||
|
// process.
|
||||||
if options.Profile != "" {
|
if options.Profile != "" {
|
||||||
profiler := performance.Track(options.Profile)
|
profiler := performance.Track(options.Profile)
|
||||||
process.On("start", profiler.Start)
|
process.On("start", profiler.Start)
|
||||||
process.On("end", profiler.End)
|
process.On("end", profiler.End)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the user selected to produce a step-by-step explanation, attach an
|
||||||
|
// observer here.
|
||||||
if options.Explanation {
|
if options.Explanation {
|
||||||
explanation.Track(process)
|
explanation.Track(process)
|
||||||
}
|
}
|
||||||
|
|
||||||
statistics := statistics.Track()
|
// If the user opted to track statistics, attach a tracker here, too.
|
||||||
process.On("start", statistics.Start)
|
if options.Statistics {
|
||||||
process.On("step", statistics.Step)
|
statistics := statistics.Track()
|
||||||
process.On("end", statistics.End)
|
process.On("start", statistics.Start)
|
||||||
|
process.On("step", statistics.Step)
|
||||||
|
process.On("end", statistics.End)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user selected for verbose debug logs, attach a reduction tracker.
|
||||||
|
if options.Verbose {
|
||||||
|
process.On("step", func() {
|
||||||
|
logger.Info("reduction", "tree", lambda.Stringify(compiled))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
process.Run()
|
process.Run()
|
||||||
|
|
||||||
|
// Return the final reduced result.
|
||||||
fmt.Println(lambda.Stringify(compiled))
|
fmt.Println(lambda.Stringify(compiled))
|
||||||
fmt.Fprint(os.Stderr, statistics.Results.String())
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// Package "cli" provides miscellaneous helper functions.
|
||||||
package cli
|
package cli
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -12,6 +13,6 @@ func HandleError(err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, "ERROR:", err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
// Package "config" parses ad handles the user settings given to the program.
|
||||||
package config
|
package config
|
||||||
|
|
||||||
// Arguments given to program.
|
// Configuration settings for the program.
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Source Source // The source code given to the program.
|
Source Source // The source code given to the program.
|
||||||
Verbose bool // Whether or not to print debug logs.
|
Verbose bool // Whether or not to print debug logs.
|
||||||
Explanation bool // Whether or not to print an explanation of the reduction.
|
Explanation bool // Whether or not to print an explanation of the reduction.
|
||||||
Profile string // If not nil, print a CPU profile during execution.
|
Profile string // If not nil, print a CPU profile during execution.
|
||||||
|
Statistics bool // Whether or not to print statistics.
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,14 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Define the correct logger for the program to use.
|
// Returns a structured logger with the appropriate configurations.
|
||||||
func (c Config) GetLogger() *slog.Logger {
|
func (c Config) GetLogger() *slog.Logger {
|
||||||
var level slog.Level
|
// By default, only print out errors.
|
||||||
|
level := slog.LevelError
|
||||||
|
|
||||||
|
// If the user set the output to be "VERBOSE", return the debug logs.
|
||||||
if c.Verbose {
|
if c.Verbose {
|
||||||
level = slog.LevelInfo
|
level = slog.LevelInfo
|
||||||
} else {
|
|
||||||
level = slog.LevelError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return slog.New(
|
return slog.New(
|
||||||
|
|||||||
@@ -7,13 +7,14 @@ import (
|
|||||||
|
|
||||||
// Extract the program configuration from the command-line arguments.
|
// Extract the program configuration from the command-line arguments.
|
||||||
func FromArgs() (*Config, error) {
|
func FromArgs() (*Config, error) {
|
||||||
// Parse flags.
|
// Relevant flags.
|
||||||
verbose := flag.Bool("v", false, "Verbosity. If set, the program will print logs.")
|
verbose := flag.Bool("v", false, "Verbosity. If set, the program will print logs.")
|
||||||
explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.")
|
explanation := flag.Bool("x", false, "Explanation. Whether or not to show all reduction steps.")
|
||||||
profile := flag.String("p", "", "CPU profiling. If set, the program will run a performance profile during execution.")
|
statistics := flag.Bool("s", false, "Statistics. If set, the process will print various statistics about the run.")
|
||||||
|
profile := flag.String("p", "", "CPU profiling. If an output file is defined, the program will profile its execution and dump its results into it.")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
// Parse non-flag arguments.
|
// There must only be one input argument.
|
||||||
if flag.NArg() == 0 {
|
if flag.NArg() == 0 {
|
||||||
return nil, fmt.Errorf("no input given")
|
return nil, fmt.Errorf("no input given")
|
||||||
} else if flag.NArg() > 1 {
|
} else if flag.NArg() > 1 {
|
||||||
@@ -25,7 +26,7 @@ func FromArgs() (*Config, error) {
|
|||||||
if flag.Arg(0) == "-" {
|
if flag.Arg(0) == "-" {
|
||||||
source = StdinSource{}
|
source = StdinSource{}
|
||||||
} else {
|
} else {
|
||||||
source = StringSource{data: flag.Arg(0)}
|
source = StringSource{Data: flag.Arg(0)}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &Config{
|
return &Config{
|
||||||
@@ -33,5 +34,6 @@ func FromArgs() (*Config, error) {
|
|||||||
Verbose: *verbose,
|
Verbose: *verbose,
|
||||||
Explanation: *explanation,
|
Explanation: *explanation,
|
||||||
Profile: *profile,
|
Profile: *profile,
|
||||||
|
Statistics: *statistics,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,21 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Defines the consumption of different types of input sources.
|
// A method of extracting input from the user.
|
||||||
type Source interface {
|
type Source interface {
|
||||||
// Get the data.
|
// Fetch data from this source.
|
||||||
Pull() (string, error)
|
Extract() (string, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
// A source coming from a string.
|
// A source defined by a string.
|
||||||
type StringSource struct{ data string }
|
type StringSource struct{ Data string }
|
||||||
|
|
||||||
func (s StringSource) Pull() (string, error) { return s.data, nil }
|
func (s StringSource) Extract() (string, error) { return s.Data, nil }
|
||||||
|
|
||||||
// A source coming from standard input.
|
// A source pulling from standard input.
|
||||||
type StdinSource struct{}
|
type StdinSource struct{}
|
||||||
|
|
||||||
func (s StdinSource) Pull() (string, error) {
|
func (s StdinSource) Extract() (string, error) {
|
||||||
data, err := io.ReadAll(os.Stdin)
|
data, err := io.ReadAll(os.Stdin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
|
|||||||
@@ -1,31 +1,31 @@
|
|||||||
package executer
|
// Package "engine" provides an extensible interface for users to interfact with
|
||||||
|
// lambda calculus.
|
||||||
|
package engine
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"log/slog"
|
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/internal/config"
|
"git.maximhutz.com/max/lambda/internal/config"
|
||||||
"git.maximhutz.com/max/lambda/pkg/emitter"
|
"git.maximhutz.com/max/lambda/pkg/emitter"
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Executor struct {
|
// A process for reducing one lambda expression.
|
||||||
|
type Engine struct {
|
||||||
Config *config.Config
|
Config *config.Config
|
||||||
Expression *lambda.Expression
|
Expression *lambda.Expression
|
||||||
emitter.Emitter
|
emitter.Emitter
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(config *config.Config, expression *lambda.Expression) *Executor {
|
// Create a new engine, given an unreduced lambda expression.
|
||||||
return &Executor{Config: config, Expression: expression}
|
func New(config *config.Config, expression *lambda.Expression) *Engine {
|
||||||
|
return &Engine{Config: config, Expression: expression}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e Executor) Run() {
|
// Begin the reduction process.
|
||||||
|
func (e Engine) Run() {
|
||||||
e.Emit("start")
|
e.Emit("start")
|
||||||
|
|
||||||
for lambda.ReduceOnce(e.Expression) {
|
for lambda.ReduceOnce(e.Expression) {
|
||||||
e.Emit("step")
|
e.Emit("step")
|
||||||
if e.Config.Verbose {
|
|
||||||
slog.Info("reduction", "tree", lambda.Stringify(*e.Expression))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
e.Emit("end")
|
e.Emit("end")
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
|
// Package "explanation" provides a observer to gather the reasoning during the
|
||||||
|
// reduction, and present a thorough explanation to the user for each step.
|
||||||
package explanation
|
package explanation
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"git.maximhutz.com/max/lambda/internal/executer"
|
"git.maximhutz.com/max/lambda/internal/engine"
|
||||||
"git.maximhutz.com/max/lambda/pkg/lambda"
|
"git.maximhutz.com/max/lambda/pkg/lambda"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Track the reductions made by a reduction proess.
|
||||||
type Tracker struct {
|
type Tracker struct {
|
||||||
process *executer.Executor
|
process *engine.Engine
|
||||||
}
|
}
|
||||||
|
|
||||||
func Track(process *executer.Executor) *Tracker {
|
// Attaches a new explanation tracker to a process.
|
||||||
|
func Track(process *engine.Engine) *Tracker {
|
||||||
tracker := &Tracker{process: process}
|
tracker := &Tracker{process: process}
|
||||||
process.On("start", tracker.Start)
|
process.On("start", tracker.Start)
|
||||||
process.On("step", tracker.Step)
|
process.On("step", tracker.Step)
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package "performance" provides a tracker to observer CPU performance during
|
||||||
|
// execution.
|
||||||
package performance
|
package performance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -6,16 +8,20 @@ import (
|
|||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Observes a reduction process, and publishes a CPU performance profile on
|
||||||
|
// completion.
|
||||||
type Tracker struct {
|
type Tracker struct {
|
||||||
File string
|
File string
|
||||||
filePointer *os.File
|
filePointer *os.File
|
||||||
Error error
|
Error error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a performance tracker that outputs a profile to "file".
|
||||||
func Track(file string) *Tracker {
|
func Track(file string) *Tracker {
|
||||||
return &Tracker{File: file}
|
return &Tracker{File: file}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Begin profiling.
|
||||||
func (t *Tracker) Start() {
|
func (t *Tracker) Start() {
|
||||||
var absPath string
|
var absPath string
|
||||||
|
|
||||||
@@ -40,6 +46,7 @@ func (t *Tracker) Start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Stop profiling.
|
||||||
func (t *Tracker) End() {
|
func (t *Tracker) End() {
|
||||||
pprof.StopCPUProfile()
|
pprof.StopCPUProfile()
|
||||||
t.filePointer.Close()
|
t.filePointer.Close()
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
// Package "statistics" provides a way to observer reduction speed during
|
||||||
|
// execution.
|
||||||
package statistics
|
package statistics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -5,15 +7,18 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Statistics for a specific reduction.
|
||||||
type Results struct {
|
type Results struct {
|
||||||
StepsTaken uint64 // Number of steps taken during execution.
|
StepsTaken uint64 // Number of steps taken during execution.
|
||||||
TimeElapsed uint64 // The time (ms) taken for execution to complete.
|
TimeElapsed uint64 // The time (ms) taken for execution to complete.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Returns the average number of operations per second of the execution.
|
||||||
func (r Results) OpsPerSecond() float32 {
|
func (r Results) OpsPerSecond() float32 {
|
||||||
return float32(r.StepsTaken) / (float32(r.TimeElapsed) / 1000)
|
return float32(r.StepsTaken) / (float32(r.TimeElapsed) / 1000)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Format the results as a string.
|
||||||
func (r Results) String() string {
|
func (r Results) String() string {
|
||||||
builder := strings.Builder{}
|
builder := strings.Builder{}
|
||||||
fmt.Fprintln(&builder, "Time Spent:", r.TimeElapsed, "ms")
|
fmt.Fprintln(&builder, "Time Spent:", r.TimeElapsed, "ms")
|
||||||
@@ -1,15 +1,18 @@
|
|||||||
package statistics
|
package statistics
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// An observer, to track reduction performance.
|
||||||
type Tracker struct {
|
type Tracker struct {
|
||||||
start time.Time
|
start time.Time
|
||||||
steps uint64
|
steps uint64
|
||||||
Results *Results
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create a new reduction performance tracker.
|
||||||
func Track() *Tracker {
|
func Track() *Tracker {
|
||||||
return &Tracker{}
|
return &Tracker{}
|
||||||
}
|
}
|
||||||
@@ -17,7 +20,6 @@ func Track() *Tracker {
|
|||||||
func (t *Tracker) Start() {
|
func (t *Tracker) Start() {
|
||||||
t.start = time.Now()
|
t.start = time.Now()
|
||||||
t.steps = 0
|
t.steps = 0
|
||||||
t.Results = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) Step() {
|
func (t *Tracker) Step() {
|
||||||
@@ -25,8 +27,10 @@ func (t *Tracker) Step() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (t *Tracker) End() {
|
func (t *Tracker) End() {
|
||||||
t.Results = &Results{
|
results := Results{
|
||||||
StepsTaken: t.steps,
|
StepsTaken: t.steps,
|
||||||
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
|
TimeElapsed: uint64(time.Since(t.start).Milliseconds()),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Fprint(os.Stderr, results.String())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ func parseStatements(i *TokenIterator) ([]ast.Statement, error) {
|
|||||||
for {
|
for {
|
||||||
if statement, err := parseStatement(i); err != nil {
|
if statement, err := parseStatement(i); err != nil {
|
||||||
break
|
break
|
||||||
} else if _, err := parseList(i, parseBreak, 1); err != nil {
|
} else if _, err := parseList(i, parseBreak, 1); err != nil && !i.Done() {
|
||||||
break
|
break
|
||||||
} else {
|
} else {
|
||||||
statements = append(statements, statement)
|
statements = append(statements, statement)
|
||||||
@@ -212,6 +212,7 @@ func parseStatement(i *TokenIterator) (ast.Statement, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Given a list of tokens, attempt to parse it into an syntax tree.
|
||||||
func Parse(tokens []token.Token) (ast.Expression, error) {
|
func Parse(tokens []token.Token) (ast.Expression, error) {
|
||||||
i := iterator.Of(tokens)
|
i := iterator.Of(tokens)
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ func stringifyClause(n *ast.Clause) string {
|
|||||||
return "{ " + stmts + Stringify(n.Returns) + " }"
|
return "{ " + stmts + Stringify(n.Returns) + " }"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Convert an expression back into valid source code.
|
||||||
func Stringify(n ast.Expression) string {
|
func Stringify(n ast.Expression) string {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *ast.Atom:
|
case *ast.Atom:
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ func getToken(i *iterator.Iterator[rune]) (*token.Token, error) {
|
|||||||
return nil, fmt.Errorf("unknown character '%v'", string(letter))
|
return nil, fmt.Errorf("unknown character '%v'", string(letter))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parses a list of runes into tokens. All error encountered are returned, as well.
|
// Parse a string into tokens.
|
||||||
func GetTokens(input string) ([]token.Token, error) {
|
func GetTokens(input string) ([]token.Token, error) {
|
||||||
i := iterator.Of([]rune(input))
|
i := iterator.Of([]rune(input))
|
||||||
tokens := []token.Token{}
|
tokens := []token.Token{}
|
||||||
|
|||||||
Reference in New Issue
Block a user