Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 142 additions & 32 deletions cmd/clank/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,14 @@ func main() {
}

func usage() {
fmt.Fprintf(os.Stderr, "Usage: clank [--json] [--pretty] [command] [flags] <file.clk>\n\n")
fmt.Fprintf(os.Stderr, "Usage: clank [--json] [--pretty] [command] [flags] <file.clk>\n")
fmt.Fprintf(os.Stderr, " clank run -c '<code>' Run inline code\n")
fmt.Fprintf(os.Stderr, " clank eval '<expr>' Evaluate an expression\n")
fmt.Fprintf(os.Stderr, " clank eval -f <file.clk> Evaluate a file\n\n")
fmt.Fprintf(os.Stderr, "Commands:\n")
fmt.Fprintf(os.Stderr, " run Run a program (default)\n")
fmt.Fprintf(os.Stderr, " check Type-check a program\n")
fmt.Fprintf(os.Stderr, " eval Evaluate and print the result\n")
fmt.Fprintf(os.Stderr, " run Run a program (default). Use -c for inline code\n")
fmt.Fprintf(os.Stderr, " check Type-check a program. Use -c for inline code\n")
fmt.Fprintf(os.Stderr, " eval Evaluate an expression and print the result\n")
fmt.Fprintf(os.Stderr, " fmt Format source code\n")
fmt.Fprintf(os.Stderr, " lint Lint source code\n")
fmt.Fprintf(os.Stderr, " doc Search and view documentation\n")
Expand Down Expand Up @@ -82,6 +85,8 @@ func run() int {
prettyMode := false
command := "run"
var file string
var inlineCode string
var evalFile string
var ruleFlags []string

var positional []string
Expand All @@ -93,6 +98,24 @@ func run() int {
checkMode = true
case "--stdin":
stdinMode = true
case "-c":
if i+1 < len(rawArgs) {
inlineCode = rawArgs[i+1]
i++
} else {
fmt.Fprintf(os.Stderr, "error: -c requires a code argument\n")
return 1
}
continue
case "-f":
if i+1 < len(rawArgs) {
evalFile = rawArgs[i+1]
i++
} else {
fmt.Fprintf(os.Stderr, "error: -f requires a file argument\n")
return 1
}
continue
case "--rule", "--filter", "--name", "--entry", "--path", "--version", "--github":
if i+1 < len(rawArgs) {
if rawArgs[i] == "--rule" {
Expand All @@ -116,43 +139,84 @@ func run() int {
}
}

if len(positional) == 0 {
if len(positional) == 0 && inlineCode == "" && evalFile == "" {
usage()
return 1
}

// Commands that handle their own file loading
switch positional[0] {
case "doc":
return cmdDoc(positional[1:], jsonOut, rawArgs)
case "test":
return cmdTest(positional[1:], jsonOut, rawArgs)
case "pkg":
return cmdPkg(positional[1:], jsonOut, rawArgs)
case "spec":
return cmdSpec()
}

// Determine command and file
switch positional[0] {
case "run", "check", "eval", "fmt", "lint", "pretty", "terse":
command = positional[0]
if (command == "fmt" || command == "pretty" || command == "terse") && stdinMode {
// fmt/pretty/terse --stdin: no file arg needed
} else if len(positional) < 2 {
fmt.Fprintf(os.Stderr, "error: %s requires a file argument\n", command)
return 1
} else {
file = positional[1]
// Determine which command we're running
if len(positional) > 0 {
switch positional[0] {
// Commands that handle their own file loading
case "doc":
return cmdDoc(positional[1:], jsonOut, rawArgs)
case "test":
return cmdTest(positional[1:], jsonOut, rawArgs)
case "pkg":
return cmdPkg(positional[1:], jsonOut, rawArgs)
case "spec":
return cmdSpec()

case "eval":
command = "eval"
// eval: remaining positional args are the expression (unless -f is used)
if evalFile == "" && inlineCode == "" {
if len(positional) < 2 {
fmt.Fprintf(os.Stderr, "error: eval requires an expression or -f <file>\n")
return 1
}
inlineCode = strings.Join(positional[1:], " ")
}

case "run", "check":
command = positional[0]
if inlineCode != "" {
// -c flag provides the source
} else if len(positional) < 2 {
fmt.Fprintf(os.Stderr, "error: %s requires a file argument or -c '<code>'\n", command)
return 1
} else {
file = positional[1]
}

case "fmt", "lint", "pretty", "terse":
command = positional[0]
if (command == "fmt" || command == "pretty" || command == "terse") && stdinMode {
// fmt/pretty/terse --stdin: no file arg needed
} else if len(positional) < 2 {
fmt.Fprintf(os.Stderr, "error: %s requires a file argument\n", command)
return 1
} else {
file = positional[1]
}

default:
// No command specified — default to "run" with file
file = positional[0]
}
default:
file = positional[0]
} else if inlineCode != "" {
// clank -e '<code>' — defaults to run
command = "run"
} else if evalFile != "" {
// clank -f <file> — defaults to eval
command = "eval"
}

// -f is only valid with eval
if evalFile != "" && command != "eval" {
fmt.Fprintf(os.Stderr, "error: -f can only be used with eval\n")
return 1
}

// Read source file (or stdin)
// Read source: inline code, eval file (-f), stdin (--stdin), or file
var source []byte
var err error
if stdinMode && file == "" {
if inlineCode != "" {
source = []byte(inlineCode)
} else if evalFile != "" {
source, err = os.ReadFile(evalFile)
file = evalFile
} else if stdinMode && file == "" {
source, err = io.ReadAll(os.Stdin)
} else {
source, err = os.ReadFile(file)
Expand All @@ -164,6 +228,11 @@ func run() int {
})
}

// eval: wrap bare expressions in a main function
if command == "eval" {
source = []byte(wrapExprSource(string(source)))
}

// Pretty/terse operate on raw source — dispatch before lex/parse
if command == "pretty" || command == "terse" {
return cmdPrettyTerse(string(source), command, file, jsonOut, stdinMode)
Expand All @@ -180,6 +249,8 @@ func run() int {
if file != "" {
absPath, _ := filepath.Abs(file)
baseDir = filepath.Dir(absPath)
} else {
baseDir, _ = os.Getwd()
}

// Lex
Expand Down Expand Up @@ -395,6 +466,45 @@ func cmdCheck(program *ast.Program, baseDir string, jsonOut bool) int {
return 1
}

// wrapExprSource wraps a bare expression in a main function for eval.
// If the source already contains a top-level definition (has ':' before '='),
// it is returned as-is.
// topLevelKeywords are keywords that can only appear at the start of a
// top-level declaration, never at the start of a bare expression.
var topLevelKeywords = map[string]bool{
"type": true, "effect": true, "pub": true, "mod": true,
"use": true, "interface": true, "impl": true, "test": true,
"affine": true, "opaque": true, "alias": true,
}

func wrapExprSource(source string) string {
trimmed := strings.TrimSpace(source)
for _, line := range strings.Split(trimmed, "\n") {
line = strings.TrimSpace(line)
if line == "" || strings.HasPrefix(line, "#") {
continue
}
// Check if line starts with a top-level keyword
fields := strings.Fields(line)
if len(fields) == 0 {
break
}
firstWord := fields[0]
if topLevelKeywords[firstWord] {
return source
}
// Check if first non-comment line looks like a definition (has ':' before '=')
colonIdx := strings.Index(line, ":")
eqIdx := strings.Index(line, "=")
if colonIdx > 0 && (eqIdx < 0 || colonIdx < eqIdx) {
return source
}
break
}
// Wrap the expression as the body of main. cmdEval will print the result.
return fmt.Sprintf("main : () -> <> auto = %s", trimmed)
}

// cmdEval compiles, links, and runs the program, printing the final result value.
func cmdEval(program *ast.Program, baseDir string, jsonOut bool) int {
linked := loader.LinkWithPackages(program, baseDir, resolvePackageModules(baseDir))
Expand Down
37 changes: 37 additions & 0 deletions cmd/clank/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package main

import "testing"

func TestWrapExprSource(t *testing.T) {
tests := []struct {
name string
input string
wantWrapped bool // true if we expect wrapping to occur
}{
{"bare expression", "1 + 2", true},
{"function call", "div(10, 2)", true},
{"string literal", `"hello"`, true},
{"type decl", "type Option<a> = Some(a) | None", false},
{"effect decl", "effect Foo { op : () -> Int }", false},
{"definition with annotation", "foo : Int = 42", false},
{"pub definition", "pub bar : Int = 1", false},
{"comment then expr", "# comment\n1 + 2", true},
{"blank lines then expr", "\n\n 1 + 2 \n", true},
{"empty string", "", true},
{"only comments", "# just a comment", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := wrapExprSource(tt.input)
wasWrapped := result != tt.input
if wasWrapped != tt.wantWrapped {
if tt.wantWrapped {
t.Errorf("expected wrapping, got: %q", result)
} else {
t.Errorf("expected no wrapping, got: %q", result)
}
}
})
}
}
Loading