diff --git a/compiler/pp/std.ch b/compiler/pp/std.ch index dcb5f4f..57dc86f 100644 --- a/compiler/pp/std.ch +++ b/compiler/pp/std.ch @@ -74,8 +74,15 @@ /* --- bulk record export --- COPY TO copies visible records of the current workarea into a fresh DBF. FIELDS/FOR/WHILE/NEXT/RECORD/REST work as in Harbour. SDF and - DELIMITED variants stay as silent no-ops in the parser until their - backends land. */ + DELIMITED variants are not implemented; the matching rules below + raise a clear runtime error so callers don't quietly get a regular + DBF copy when they asked for an SDF dump. Order matters: the SDF / + DELIMITED rules must come before the regular COPY rule. */ +#command COPY [TO <(f)>] [FIELDS ] SDF [<*tail*>] => ; + __dbNotImpl("COPY TO ... SDF") +#command COPY [TO <(f)>] [FIELDS ] DELIMITED [<*tail*>] => ; + __dbNotImpl("COPY TO ... DELIMITED") + #command COPY [TO <(f)>] [FIELDS ] ; [FOR ] [WHILE ] [NEXT ] ; [RECORD ] [] [ALL] => ; @@ -94,8 +101,19 @@ /* --- console output --- LIST emits every record matching the filter; DISPLAY without ALL shows just the current record. Both share __dbList — lAll - distinguishes them. TO PRINTER / TO FILE accepted but unused; - stdout is the only sink for now. */ + distinguishes them. TO PRINTER / TO FILE redirection is not yet + implemented; the stub rules below surface a clear error rather + than silently sending output to stdout when a printer/file was + requested. Order matters: more specific rules first. */ +#command LIST [] TO PRINTER [<*tail*>] => ; + __dbNotImpl("LIST ... TO PRINTER") +#command LIST [] TO FILE <(f)> [<*tail*>] => ; + __dbNotImpl("LIST ... TO FILE") +#command DISPLAY [] TO PRINTER [<*tail*>] => ; + __dbNotImpl("DISPLAY ... TO PRINTER") +#command DISPLAY [] TO FILE <(f)> [<*tail*>] => ; + __dbNotImpl("DISPLAY ... TO FILE") + #command LIST [] [] ; [FOR ] [WHILE ] [NEXT ] ; [RECORD ] [] [ALL] => ; @@ -114,10 +132,14 @@ must already be sorted/indexed on the key for the grouping to produce one row per distinct value. - Note for callers: Five doesn't auto-resolve a bare identifier - inside a code block to the current workarea's field. Write the - key alias-qualified — `ON src->dept` rather than `ON dept`. */ -#command TOTAL [TO <(f)>] [ON ] [FIELDS ] ; + Note on key syntax — TOTAL evaluates `` only in the source + workarea, so `<{key}>` (verbatim blockify) is enough; user can + write `ON src->dept` (alias-qualified) or `ON _FIELD->dept` + (current-area). UPDATE FROM evaluates the key block in BOTH + master and detail context and therefore needs `_FIELD->`-wrapped + bare keys instead — the two rules look superficially similar but + their evaluation contexts differ. */ +#command TOTAL TO <(f)> ON [FIELDS ] ; [FOR ] [WHILE ] [NEXT ] ; [RECORD ] [] [ALL] => ; __dbTotal( <(f)>, <{key}>, { <(fields)> }, ; diff --git a/hbrtl/database.go b/hbrtl/database.go index 555f64c..b8b9949 100644 --- a/hbrtl/database.go +++ b/hbrtl/database.go @@ -770,6 +770,24 @@ func rtlDbAverage(t *hbrt.Thread) { t.RetDouble(sum/float64(n), 10, 2) } +// rtlDbNotImpl raises a runtime error explaining which xBase clause +// the user invoked that Five doesn't yet implement. std.ch routes +// SDF / DELIMITED / TO PRINTER / TO FILE variants here so they fail +// loudly with a helpful diagnostic instead of being silently dropped. +func rtlDbNotImpl(t *hbrt.Thread) { + nParams := t.ParamCount() + t.Frame(nParams, 0) + defer t.EndProcFast() + what := "" + if nParams >= 1 && t.Local(1).IsString() { + what = t.Local(1).AsString() + } + panic(&hbrt.HbError{ + Description: "xBase clause not implemented: " + what, + SubSystem: "BASE", + }) +} + // rtlDbCopy implements __dbCopy(cFile, aFields, bFor, bWhile, nNext, // xRec, lRest) — copy visible records from the current workarea into a // freshly created DBF. Field projection: an empty/missing aFields @@ -989,10 +1007,13 @@ func rtlDbSort(t *hbrt.Thread) { return } - // Parse sort-key spec: name[/D] entries → (fieldIdx, descending). + // Parse sort-key spec: name[/D|/A|/C[/D]|/D[/C]] entries. + // /D = descending, /A = ascending (default), /C = case-insensitive. + // /C and /D can be combined — `name/CD` or `name/DC` both ok. type sortKey struct { - idx int - desc bool + idx int + desc bool + caseFold bool } var keys []sortKey if nParams >= 2 && t.Local(2).IsArray() { @@ -1002,13 +1023,39 @@ func rtlDbSort(t *hbrt.Thread) { continue } desc := false - // Suffix `/D` (descending), `/A` (ascending), `/C` - // (case-insensitive — treated as ascending). - if i := strings.LastIndexByte(s, '/'); i > 0 { + caseFold := false + // Strip suffixes — may be multiple, e.g. `name/C/D` or + // `name/CD`. Walk left from the last `/` repeatedly. + for { + i := strings.LastIndexByte(s, '/') + if i < 0 { + break + } suffix := strings.ToUpper(strings.TrimSpace(s[i+1:])) - switch suffix { - case "D": - desc = true + if suffix == "" { + break + } + done := false + for _, ch := range suffix { + switch ch { + case 'D': + desc = true + case 'A': + // ascending — explicit no-op + case 'C': + caseFold = true + default: + // Unknown letter — leave the suffix attached + // to the name and stop parsing so a field + // like `name/foo` doesn't get silently mangled. + done = true + } + if done { + break + } + } + if done { + break } s = strings.TrimSpace(s[:i]) } @@ -1021,7 +1068,7 @@ func rtlDbSort(t *hbrt.Thread) { } } if idx >= 0 { - keys = append(keys, sortKey{idx: idx, desc: desc}) + keys = append(keys, sortKey{idx: idx, desc: desc, caseFold: caseFold}) } } } @@ -1083,13 +1130,26 @@ func rtlDbSort(t *hbrt.Thread) { // Sort if any keys were given. Stable so equal keys keep input // order. Comparison is type-aware: numeric by AsNumDouble, date by - // AsNumInt julian, logical by truth, otherwise string. + // AsNumInt julian, logical by truth, otherwise string. /C keys + // fold case before string comparison. if len(keys) > 0 && len(rows) > 1 { less := func(i, j int) bool { for _, k := range keys { a := rows[i][k.idx] b := rows[j][k.idx] - cmp := compareValues(a, b) + var cmp int + if k.caseFold && a.IsString() && b.IsString() { + sa := strings.ToLower(a.AsString()) + sb := strings.ToLower(b.AsString()) + switch { + case sa < sb: + cmp = -1 + case sa > sb: + cmp = 1 + } + } else { + cmp = compareValues(a, b) + } if cmp == 0 { continue } diff --git a/hbrtl/register.go b/hbrtl/register.go index f5c7973..c82a79e 100644 --- a/hbrtl/register.go +++ b/hbrtl/register.go @@ -205,6 +205,7 @@ func RegisterRTL(vm *hbrt.VM) { hbrt.Sym("__DBTOTAL", hbrt.FsPublic, rtlDbTotal), hbrt.Sym("__DBJOIN", hbrt.FsPublic, rtlDbJoin), hbrt.Sym("__DBUPDATE", hbrt.FsPublic, rtlDbUpdate), + hbrt.Sym("__DBNOTIMPL", hbrt.FsPublic, rtlDbNotImpl), hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter), hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter), hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter),