feat: sonya now writes out more comprehensible error messages

This commit is contained in:
2026-04-23 21:52:26 +09:00
parent eb851e49fa
commit 354186bc33
4 changed files with 50 additions and 32 deletions
+7 -8
View File
@@ -17,21 +17,20 @@ struct sonya {
case .isEmpty: case .isEmpty:
print("sonya: sonyafile is empty") print("sonya: sonyafile is empty")
exit(2) exit(2)
case .syntaxError: case .syntaxError(let errs):
print("sonya: syntax error in sonyafile") for err in errs {
print("line '\(err)' is unrecognized")
}
exit(3) exit(3)
case .noSuchRecipe: case .noSuchRecipe:
print("sonya: no recipe named '\(recipe)'") print("sonya: no recipe named '\(recipe)'")
exit(4) exit(4)
case .noSuchDependency: case .shellReturnedError(let code):
print("sonya: unknown dependency") print("sonya: command failed with code \(code)")
exit(5) exit(5)
case .shellReturnedError:
print("sonya: command failed")
exit(6)
case .noSuchVariable: case .noSuchVariable:
print("sonya: undefined variable referenced in recipe '\(recipe)'") print("sonya: undefined variable referenced in recipe '\(recipe)'")
exit(7) exit(6)
} }
} }
} }
@@ -7,14 +7,13 @@
import Foundation import Foundation
public enum runResult { public enum runResult: Equatable {
case OK case OK
case noSonyafile case noSonyafile
case isEmpty case isEmpty
case syntaxError case syntaxError(errs: [String])
case noSuchRecipe case noSuchRecipe
case noSuchDependency case shellReturnedError(Int32)
case shellReturnedError
case noSuchVariable case noSuchVariable
} }
@@ -24,23 +23,28 @@ public func runRecipe(_ recipeName: String, args: [String] = []) -> runResult {
let tokenized = Lexer().tokenize(sonyafile) let tokenized = Lexer().tokenize(sonyafile)
guard !tokenized.isEmpty else { return .isEmpty } guard !tokenized.isEmpty else { return .isEmpty }
guard let ast = Parser().parse(tokenized) else { return .syntaxError } // guard let ast = Parser().parse(tokenized) else { return .syntaxError }
switch Parser().parse(tokenized) {
guard let requestedRecipe = ast.rules[recipeName] else { return .noSuchRecipe } case .success(let ast):
// check for dependencies guard let requestedRecipe = ast.rules[recipeName] else { return .noSuchRecipe }
if !requestedRecipe.dependencies.isEmpty { // check for dependencies
for dependency in requestedRecipe.dependencies { if !requestedRecipe.dependencies.isEmpty {
let result = runRecipe(dependency) for dependency in requestedRecipe.dependencies {
guard result == .OK else { return result } let result = runRecipe(dependency)
guard result == .OK else { return result }
}
} }
for command in requestedRecipe.commands {
guard let resolved = resolve(command, vars: ast.vars) else { return .noSuchVariable }
let exitCode = shell(resolved)
guard exitCode == 0 else { return .shellReturnedError(exitCode) }
}
// all good
return .OK
case .failure(let error):
return .syntaxError(errs: error.errorLines)
} }
for command in requestedRecipe.commands {
guard let resolved = resolve(command, vars: ast.vars) else { return .noSuchVariable }
guard shell(resolved) == 0 else { return .shellReturnedError }
}
// all good
return .OK
} }
@@ -7,6 +7,7 @@ enum Token : Equatable {
case variable(name: String, value: String) case variable(name: String, value: String)
case comment(String) case comment(String)
case newline case newline
case error(line: String)
} }
struct Lexer { struct Lexer {
@@ -75,7 +76,7 @@ struct Lexer {
return [.command(String(match.1))] return [.command(String(match.1))]
} }
return [] // unrecognized return [.error(line: line)] // unrecognized
} }
} }
+17 -3
View File
@@ -1,3 +1,11 @@
struct errorLines: Error {
var errorLines: [String]
init(_ errs: [String]) {
errorLines = errs
}
}
struct sonyaAST { struct sonyaAST {
var vars: [String : String] = [:] var vars: [String : String] = [:]
var rules: [String : Rule] = [:] var rules: [String : Rule] = [:]
@@ -9,8 +17,9 @@ struct Rule {
} }
struct Parser { struct Parser {
func parse(_ tokens: [Token]) -> sonyaAST? { func parse(_ tokens: [Token]) -> Result<sonyaAST, errorLines> {
var ast = sonyaAST() var ast = sonyaAST()
var errors: [String] = []
var currentRule: (name: String, rule: Rule)? = nil var currentRule: (name: String, rule: Rule)? = nil
for token in tokens { for token in tokens {
@@ -26,6 +35,8 @@ struct Parser {
currentRule?.rule.dependencies.append(name) currentRule?.rule.dependencies.append(name)
case .command(let cmd): case .command(let cmd):
currentRule?.rule.commands.append(cmd) currentRule?.rule.commands.append(cmd)
case .error(let line):
errors.append(line)
case .newline, .comment: case .newline, .comment:
continue continue
} }
@@ -33,7 +44,10 @@ struct Parser {
if let rule = currentRule { ast.rules[rule.name] = rule.rule } if let rule = currentRule { ast.rules[rule.name] = rule.rule }
currentRule = nil currentRule = nil
return ast if errors.isEmpty {
return .success(ast)
} else {
return .failure(errorLines(errors))
}
} }
} }