c65gm/internal/utils/utils.go

263 lines
5.7 KiB
Go

package utils
import (
"fmt"
"strconv"
"strings"
)
// ParseParams splits a line into space-separated parameters, respecting quoted strings
func ParseParams(s string) ([]string, error) {
s = strings.TrimSpace(s)
if s == "" {
return []string{}, nil
}
var params []string
var current strings.Builder
inString := false
for i := 0; i < len(s); i++ {
ch := s[i]
if ch == '"' {
inString = !inString
current.WriteByte(ch)
continue
}
if !inString && (ch == ' ' || ch == '\t') {
if current.Len() > 0 {
params = append(params, current.String())
current.Reset()
}
} else {
current.WriteByte(ch)
}
}
if current.Len() > 0 {
params = append(params, current.String())
}
if inString {
return nil, fmt.Errorf("unterminated string")
}
return params, nil
}
// NormalizeSpaces reduces multiple spaces to single space, respecting quoted strings
func NormalizeSpaces(s string) string {
s = strings.TrimSpace(s)
var result strings.Builder
inString := false
lastWasSpace := false
for i := 0; i < len(s); i++ {
ch := s[i]
if ch == '"' {
inString = !inString
result.WriteByte(ch)
lastWasSpace = false
continue
}
if !inString {
if ch == ' ' || ch == '\t' {
if !lastWasSpace {
result.WriteByte(' ')
lastWasSpace = true
}
} else {
result.WriteByte(ch)
lastWasSpace = false
}
} else {
result.WriteByte(ch)
lastWasSpace = false
}
}
return result.String()
}
// IsStringLiteral checks if s is a quoted string
func IsStringLiteral(s string) bool {
l := len(s)
return l >= 2 && s[0] == '"' && s[l-1] == '"'
}
// StripQuotes removes surrounding quotes from a string literal
func StripQuotes(s string) string {
if IsStringLiteral(s) {
return s[1 : len(s)-1]
}
return s
}
// ToUpper converts string to uppercase
func ToUpper(s string) string {
return strings.ToUpper(s)
}
// ValidateIdentifier checks if s is a valid identifier (starts with letter/underscore, continues with alphanumeric/underscore)
func ValidateIdentifier(s string) bool {
if len(s) == 0 {
return false
}
first := s[0]
if !((first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_') {
return false
}
for i := 1; i < len(s); i++ {
ch := s[i]
if !((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || (ch >= '0' && ch <= '9') || ch == '_') {
return false
}
}
return true
}
// ConstantLookup is a function type for looking up constant values by name
type ConstantLookup func(name string) (value int64, found bool)
// EvaluateExpression evaluates a simple left-to-right expression
// Supports:
// - Decimal: 123
// - Hex: $FF
// - Binary: !11111111
// - Constants: MAXVAL (via lookup function)
// - Operators: +, -, *, /, | (binary OR), & (binary AND)
//
// Returns value and error. If lookup is nil, constants are not supported.
func EvaluateExpression(expr string, lookup ConstantLookup) (int64, error) {
expr = strings.TrimSpace(expr)
if expr == "" {
return 0, fmt.Errorf("empty expression")
}
// Split expression into terms and operators
var terms []string
var operators []rune
var currentTerm strings.Builder
for i := 0; i < len(expr); i++ {
ch := expr[i]
if isOperator(ch) {
// Save current term
if currentTerm.Len() > 0 {
terms = append(terms, strings.TrimSpace(currentTerm.String()))
currentTerm.Reset()
}
operators = append(operators, rune(ch))
} else {
currentTerm.WriteByte(ch)
}
}
// Save final term
if currentTerm.Len() > 0 {
terms = append(terms, strings.TrimSpace(currentTerm.String()))
}
if len(terms) == 0 {
return 0, fmt.Errorf("no terms in expression")
}
if len(operators) != len(terms)-1 {
return 0, fmt.Errorf("mismatched operators and terms")
}
// Evaluate first term
result, err := evaluateTerm(terms[0], lookup)
if err != nil {
return 0, fmt.Errorf("term %q: %w", terms[0], err)
}
// Apply operators left-to-right
for i, op := range operators {
nextVal, err := evaluateTerm(terms[i+1], lookup)
if err != nil {
return 0, fmt.Errorf("term %q: %w", terms[i+1], err)
}
switch op {
case '+':
result = result + nextVal
case '-':
result = result - nextVal
case '*':
result = result * nextVal
case '/':
if nextVal == 0 {
return 0, fmt.Errorf("division by zero")
}
result = result / nextVal
case '|':
result = result | nextVal
case '&':
result = result & nextVal
default:
return 0, fmt.Errorf("unknown operator %q", op)
}
}
return result, nil
}
// isOperator checks if a character is an operator
func isOperator(ch byte) bool {
return ch == '+' || ch == '-' || ch == '*' || ch == '/' || ch == '|' || ch == '&'
}
// evaluateTerm evaluates a single term (number or constant)
func evaluateTerm(term string, lookup ConstantLookup) (int64, error) {
term = strings.TrimSpace(term)
if term == "" {
return 0, fmt.Errorf("empty term")
}
// Check for hex: $FF
if strings.HasPrefix(term, "$") {
val, err := strconv.ParseInt(term[1:], 16, 64)
if err != nil {
return 0, fmt.Errorf("invalid hex number: %w", err)
}
return val, nil
}
// Check for binary: %11111111
if strings.HasPrefix(term, "%") {
val, err := strconv.ParseInt(term[1:], 2, 64)
if err != nil {
return 0, fmt.Errorf("invalid binary number: %w", err)
}
return val, nil
}
// Check for identifier (constant)
first := term[0]
if (first >= 'a' && first <= 'z') || (first >= 'A' && first <= 'Z') || first == '_' {
if lookup == nil {
return 0, fmt.Errorf("constant %q not supported (no lookup function)", term)
}
val, found := lookup(term)
if !found {
return 0, fmt.Errorf("constant %q not found", term)
}
return val, nil
}
// Decimal number
val, err := strconv.ParseInt(term, 10, 64)
if err != nil {
return 0, fmt.Errorf("invalid decimal number: %w", err)
}
return val, nil
}