Four audit findings around correctness/consistency in std.ch and the SORT/UPDATE/TOTAL handlers: * #13: TOTAL/UPDATE key idiom inconsistency documented as inherent. TOTAL evaluates `<key>` only in the source workarea so verbatim `<{key}>` (alias-qualified or `_FIELD->`-prefixed by the user) works. UPDATE evaluates the same block in BOTH master and detail context, so it must wrap as `_FIELD-><key>` to dispatch to whichever WA is selected at eval time. The two rules look alike but their evaluation contexts differ — also documented in std.ch alongside both rules so the asymmetry isn't a surprise. Plus: TOTAL TO and ON are now mandatory (matching the COUNT/ UPDATE pattern from Wave 1) — bare TOTAL would have produced broken syntax via the unconditional `<(f)>`/`<{key}>` template references. * #15/#16: SDF / DELIMITED variants of COPY and TO PRINTER / TO FILE variants of LIST / DISPLAY are now matched by stub rules (placed *before* the regular rules so they win) that expand to a new `__dbNotImpl(reason)` RTL primitive raising a clear `&hbrt.HbError`. BEGIN SEQUENCE / RECOVER catches the panic, so callers get a real error instead of the previous silent dispatch-to-regular-DBF-copy. * #19: SORT /C (case-insensitive) now actually folds case before the string compare, instead of being silently treated as ascending. Suffix parser also rebuilt as a multi-letter scanner so `name/CD`, `name/DC`, `name/C/D`, `name/D/C` all parse the same way — combine /C and /D freely. Unknown suffix letters (e.g., `name/X`) leave the suffix attached to the field name so a stray slash in user input doesn't get silently mangled into a broken field reference. * #27 SET DELETED: verified with a regression test that `SET DELETED ON` causes COUNT/COPY (and by extension SORT/TOTAL/JOIN/UPDATE — all of which iterate via Area.Skip) to skip rows marked deleted. The filtering is implemented at the workarea level (skipFilter in dbf.go honors hbrdd.IsSetDeleted) so no RTL changes were needed; this commit just adds the coverage so the behavior doesn't silently regress. Gates green: go test ./... : PASS FiveSql2 SQL:1999 : 43/43 Harbour compat : 56/56 Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
190 lines
9.0 KiB
Plaintext
190 lines
9.0 KiB
Plaintext
/*
|
|
* std.ch — Five standard preprocessor rules
|
|
*
|
|
* Equivalent to harbour-core/include/std.ch. Translates xBase legacy
|
|
* commands into function calls so the parser does not have to know
|
|
* about them. Auto-loaded by compiler/pp at startup.
|
|
*
|
|
* Phase A: only rules whose backend RTL function already exists in
|
|
* Five. Rules whose backend is not yet implemented (COPY, SORT,
|
|
* COUNT, SUM, AVERAGE, TOTAL, JOIN, LIST, DISPLAY, LABEL, REPORT,
|
|
* DIR) are deliberately NOT included here — the parser still handles
|
|
* them as silent no-ops until their RTL backend lands.
|
|
*
|
|
* Copyright (c) 2026 Charles KWON OhJun (charleskwonohjun@gmail.com)
|
|
* All rights reserved.
|
|
*/
|
|
|
|
/* --- file system --- */
|
|
#command ERASE <(f)> => FErase(<(f)>)
|
|
#command DELETE FILE <(f)> => FErase(<(f)>)
|
|
#command RENAME <(s)> TO <(d)> => FRename(<(s)>, <(d)>)
|
|
|
|
/* --- workarea lifecycle ---
|
|
Order matters: literal-keyword forms first, then bare CLOSE,
|
|
then the alias-form last so it doesn't shadow the others. */
|
|
#command CLOSE ALL => DbCloseAll()
|
|
#command CLOSE DATABASES => DbCloseAll()
|
|
#command CLOSE => DbCloseArea()
|
|
#command CLOSE <a> => <a>->( DbCloseArea() )
|
|
|
|
/* --- record state --- */
|
|
#command COMMIT => DbCommit()
|
|
#command UNLOCK ALL => DbUnlock()
|
|
#command UNLOCK => DbRUnlock()
|
|
|
|
/* --- record search --- */
|
|
#command LOCATE [FOR <for>] [WHILE <while>] ;
|
|
[NEXT <next>] [RECORD <rec>] [<rest:REST>] [ALL] => ;
|
|
__dbLocate(<{for}>, <{while}>, <next>, <rec>, <.rest.>)
|
|
|
|
#command CONTINUE => __dbContinue()
|
|
|
|
/* --- analytical (no extra RTL — just dbEval) ---
|
|
These mirror Harbour's std.ch but use single-value forms. Multi-
|
|
expression SUM/AVERAGE (`SUM x, y TO sx, sy`) use optional-repeat
|
|
syntax in Harbour and can be added here once a real test exercises
|
|
the more elaborate form. */
|
|
/* COUNT/SUM/AVERAGE require TO <var> — without it the rewrite
|
|
would produce naked assignment with no LHS. Match Harbour
|
|
std.ch which also makes TO non-optional. */
|
|
#command COUNT TO <v> [FOR <for>] [WHILE <while>] ;
|
|
[NEXT <next>] [RECORD <rec>] [<rest:REST>] [ALL] => ;
|
|
<v> := 0 ; dbEval( {|| <v> := <v> + 1 }, ;
|
|
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
|
|
|
/* SUM and AVERAGE accept multiple paired expressions/destinations:
|
|
`SUM x, y, z TO sx, sy, sz`. The optional `[, <xN>]` and
|
|
`[, <vN>]` repeats are matched pairwise; the result template's
|
|
chained `<v1> :=[ <vN> :=] 0` and comma-list inside the dbEval
|
|
block expand once per extra pair. Single-pair usage is unchanged. */
|
|
#command SUM <x1> [, <xN>] TO <v1> [, <vN>] ;
|
|
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
|
[RECORD <rec>] [<rest:REST>] [ALL] => ;
|
|
<v1> :=[ <vN> :=] 0 ; ;
|
|
dbEval( {|| <v1> := <v1> + <x1>[, <vN> := <vN> + <xN>] }, ;
|
|
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
|
|
|
#command AVERAGE <x> TO <v> ;
|
|
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
|
[RECORD <rec>] [<rest:REST>] [ALL] => ;
|
|
<v> := __dbAverage( <{x}>, ;
|
|
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
|
|
|
/* --- 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 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 <fields,...>] SDF [<*tail*>] => ;
|
|
__dbNotImpl("COPY TO ... SDF")
|
|
#command COPY [TO <(f)>] [FIELDS <fields,...>] DELIMITED [<*tail*>] => ;
|
|
__dbNotImpl("COPY TO ... DELIMITED")
|
|
|
|
#command COPY [TO <(f)>] [FIELDS <fields,...>] ;
|
|
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
|
[RECORD <rec>] [<rest:REST>] [ALL] => ;
|
|
__dbCopy( <(f)>, { <(fields)> }, ;
|
|
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
|
|
|
/* SORT TO copies the visible records into a fresh DBF in key order.
|
|
Each key in `<fields>` may carry `/D` for descending; default is
|
|
ascending. */
|
|
#command SORT [TO <(f)>] [ON <fields,...>] ;
|
|
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
|
[RECORD <rec>] [<rest:REST>] [ALL] => ;
|
|
__dbSort( <(f)>, { <(fields)> }, ;
|
|
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
|
|
|
/* --- 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 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 [<v,...>] TO PRINTER [<*tail*>] => ;
|
|
__dbNotImpl("LIST ... TO PRINTER")
|
|
#command LIST [<v,...>] TO FILE <(f)> [<*tail*>] => ;
|
|
__dbNotImpl("LIST ... TO FILE")
|
|
#command DISPLAY [<v,...>] TO PRINTER [<*tail*>] => ;
|
|
__dbNotImpl("DISPLAY ... TO PRINTER")
|
|
#command DISPLAY [<v,...>] TO FILE <(f)> [<*tail*>] => ;
|
|
__dbNotImpl("DISPLAY ... TO FILE")
|
|
|
|
#command LIST [<v,...>] [<off:OFF>] ;
|
|
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
|
[RECORD <rec>] [<rest:REST>] [ALL] => ;
|
|
__dbList( <.off.>, { <{v}> }, .T., ;
|
|
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
|
|
|
#command DISPLAY [<v,...>] [<off:OFF>] ;
|
|
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
|
[RECORD <rec>] [<rest:REST>] [<all:ALL>] => ;
|
|
__dbList( <.off.>, { <{v}> }, <.all.>, ;
|
|
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
|
|
|
/* TOTAL TO writes one record per consecutive run of equal key values
|
|
from the source. Numeric fields named in FIELDS are summed; every
|
|
other (non-memo) field takes the first record's value. The source
|
|
must already be sorted/indexed on the key for the grouping to
|
|
produce one row per distinct value.
|
|
|
|
Note on key syntax — TOTAL evaluates `<key>` 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 <key> [FIELDS <fields,...>] ;
|
|
[FOR <for>] [WHILE <while>] [NEXT <next>] ;
|
|
[RECORD <rec>] [<rest:REST>] [ALL] => ;
|
|
__dbTotal( <(f)>, <{key}>, { <(fields)> }, ;
|
|
<{for}>, <{while}>, <next>, <rec>, <.rest.> )
|
|
|
|
/* JOIN merges the current ("master") workarea with the named
|
|
detail alias into a fresh DBF, emitting one output row per
|
|
master/detail pair where FOR evaluates true. */
|
|
#command JOIN [WITH <(alias)>] [TO <(f)>] [FIELDS <fields,...>] ;
|
|
[FOR <for>] => ;
|
|
__dbJoin( <(alias)>, <(f)>, { <(fields)> }, <{for}> )
|
|
|
|
/* UPDATE FROM walks the named detail alias and applies the
|
|
REPLACE ... WITH ... clauses to the matching master record.
|
|
Both areas should be sorted on the key for the default forward-
|
|
walk; pass RANDOM to scan master from top for each detail key.
|
|
|
|
Note 1: ON <key> is wrapped as `_FIELD-><key>` rather than the bare
|
|
`<{key}>` Harbour uses, because the same block must evaluate
|
|
against both master and detail. Bare identifiers don't auto-bind
|
|
to fields under Five — `_FIELD->` makes the dispatch explicit.
|
|
|
|
Note 2: FROM/ON/REPLACE are all required (Harbour technically allows
|
|
them in any order but every real call site provides all three). The
|
|
former optional brackets allowed compile-clean garbage like a bare
|
|
`UPDATE` to expand to a broken-syntax call. Keep them mandatory. */
|
|
#command UPDATE FROM <(alias)> ON <key> [<rand:RANDOM>] ;
|
|
REPLACE <f1> WITH <x1> [, <fN> WITH <xN>] => ;
|
|
__dbUpdate( <(alias)>, {|| _FIELD-><key> }, <.rand.>, ;
|
|
{|| _FIELD-><f1> := <x1>[, _FIELD-><fN> := <xN>] } )
|
|
|
|
/* --- bulk maintenance --- */
|
|
#command REINDEX => DbReindex()
|
|
#command PACK => DbPack()
|
|
#command ZAP => DbZap()
|
|
|
|
/* --- input / shell --- */
|
|
#command KEYBOARD <text> => Keyboard(<text>)
|
|
#command RUN <*cmd*> => hb_Run(<(cmd)>)
|
|
|
|
/* --- legacy GET system ---
|
|
MENU TO is intentionally absent: it requires the @ PROMPT statement
|
|
companion which Five doesn't implement. Adding the rule would let
|
|
user code compile and then panic at runtime on the missing
|
|
__MenuTo() symbol. Keep the parser's silent no-op for MENU TO until
|
|
@ PROMPT lands. */
|
|
#command CLEAR GETS => GetList := {}
|