diff --git a/compiler/pp/pp.go b/compiler/pp/pp.go index a805bef..0eb0fcc 100644 --- a/compiler/pp/pp.go +++ b/compiler/pp/pp.go @@ -457,15 +457,105 @@ func (pp *Preprocessor) resolveInclude(currentFile, inclFile string) string { return "" } +// hasTopLevelSemi reports whether s contains a `;` outside of any +// string literal or paren/bracket/brace nesting. Used by applyRules +// to decide whether a line carries multiple PRG statements. +func hasTopLevelSemi(s string) bool { + depth := 0 + inStr := byte(0) + for i := 0; i < len(s); i++ { + c := s[i] + if inStr != 0 { + if c == inStr { + inStr = 0 + } + continue + } + switch c { + case '"', '\'': + inStr = c + case '(', '[', '{': + depth++ + case ')', ']', '}': + if depth > 0 { + depth-- + } + case ';': + if depth == 0 { + return true + } + } + } + return false +} + +// splitTopLevelSemi splits s on top-level `;`, respecting string +// literals and paren/bracket/brace nesting. Empty trailing splits +// (caused by a trailing `;`) are preserved so the caller can rejoin +// without losing the separator's significance for line-continuation. +func splitTopLevelSemi(s string) []string { + var parts []string + depth := 0 + inStr := byte(0) + start := 0 + for i := 0; i < len(s); i++ { + c := s[i] + if inStr != 0 { + if c == inStr { + inStr = 0 + } + continue + } + switch c { + case '"', '\'': + inStr = c + case '(', '[', '{': + depth++ + case ')', ']', '}': + if depth > 0 { + depth-- + } + case ';': + if depth == 0 { + parts = append(parts, s[start:i]) + start = i + 1 + } + } + } + parts = append(parts, s[start:]) + return parts +} + // applyRules applies #command and #translate rules to a line. // #command rules are tried first (they match complete statements). // #translate rules are tried on any part of a line. +// +// `;`-separated statements share a line in PRG (`dbCommit(); CLOSE +// ALL`); each sub-statement is matched against the rule list +// independently. Without this, only the first statement on the line +// would have rules applied, and subsequent ones would reach the +// parser unrewritten — `CLOSE ALL` after a semicolon used to fall +// through to the parser as IDENT tokens, blowing up at runtime +// when "CLOSE" tried to dispatch as a function name. func (pp *Preprocessor) applyRules(line string) string { trimmed := strings.TrimSpace(line) if trimmed == "" || strings.HasPrefix(trimmed, "//") { return line } + // Multi-statement line: split on top-level `;` (paren / string + // balanced), apply rules to each segment, rejoin. + if hasTopLevelSemi(trimmed) { + parts := splitTopLevelSemi(line) + if len(parts) > 1 { + out := make([]string, len(parts)) + for i, p := range parts { + out[i] = pp.applyRules(p) + } + return strings.Join(out, ";") + } + } + // Try #command rules (match from start of line) for _, rule := range pp.commands { if result, ok := rule.MatchLine(trimmed); ok {