diff --git a/compiler/gengo/gengo.go b/compiler/gengo/gengo.go index 7cd9975..4cf9c08 100644 --- a/compiler/gengo/gengo.go +++ b/compiler/gengo/gengo.go @@ -491,23 +491,64 @@ func (g *Generator) emitStmt(stmt ast.Stmt, locals localMap) { g.indent-- g.writeln("}") case *ast.SetCmd: + upper := strings.ToUpper(s.Setting) + + // Boolean SET toggles — call RTL Set function, no workarea needed + setFuncMap := map[string]string{ + "DELETED": "SETDELETED", + "EXACT": "SETEXACT", + "SOFTSEEK": "SETSOFTSEEK", + "EXCLUSIVE": "SETEXCLUSIVE", + "FIXED": "SETFIXED", + "CANCEL": "SETCANCEL", + "BELL": "SETBELL", + "CONFIRM": "SETCONFIRM", + "INSERT": "SETINSERT", + "ESCAPE": "SETESCAPE", + "WRAP": "SETWRAP", + } + if funcName, ok := setFuncMap[upper]; ok { + onOff := strings.ToUpper(s.Extra) + if onOff == "ON" || onOff == "OFF" { + val := "true" + if onOff == "OFF" { + val = "false" + } + g.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, funcName)) + g.writeln("t.PushNil()") + g.writeln(fmt.Sprintf("t.PushBool(%s)", val)) + g.writeln("t.Do(1)") + } + break + } + + // Value SET commands — SET DATE/DECIMALS/EPOCH TO expr + valueFuncMap := map[string]string{ + "DATE": "__SETDATEFORMAT", + "DECIMALS": "SETDECIMALS", + "EPOCH": "SETEPOCH", + } + if funcName, ok := valueFuncMap[upper]; ok && s.Expr != nil { + g.writeln(fmt.Sprintf(`t.PushSymbol(t.VM().FindSymbol(%q))`, funcName)) + g.writeln("t.PushNil()") + g.emitExpr(s.Expr) + g.writeln("t.Do(1)") + break + } + + // Workarea-specific SET commands g.writeln("{") g.indent++ g.writeln("wa := t.WA.(*hbrdd.WorkAreaManager)") g.writeln("if area := wa.Current(); area != nil {") g.indent++ - upper := strings.ToUpper(s.Setting) switch upper { case "FILTER": if s.Expr != nil { - g.writeln("if flt, ok := area.(hbrdd.Filterer); ok {") - g.indent++ g.emitExpr(s.Expr) - g.writeln(`flt.SetFilter(t.Pop2().AsString(), nil)`) - g.indent-- - g.writeln("}") + g.writeln(`area.SetFilter(t.Pop2().AsString(), nil)`) } else { - g.writeln("if flt, ok := area.(hbrdd.Filterer); ok { flt.ClearFilter() }") + g.writeln("area.ClearFilter()") } case "ORDER": if s.Expr != nil { diff --git a/compiler/parser/parser.go b/compiler/parser/parser.go index 9ff82c3..7538882 100644 --- a/compiler/parser/parser.go +++ b/compiler/parser/parser.go @@ -1706,10 +1706,23 @@ func (p *Parser) parseSet() *ast.SetCmd { var extra string // SET commands: consume everything until end of line. - // Values like "GR+/B, W+/BG" can't be parsed as expressions. - // SET FILTER TO is special — the condition IS an expression. + // Boolean toggles: SET DELETED ON/OFF, SET EXACT ON/OFF, etc. + // Value settings: SET FILTER TO expr, SET ORDER TO n, SET DATE TO fmt upperSetting := strings.ToUpper(setting) - if p.match(token.TO) { + + // Check for ON/OFF boolean toggle + booleanSets := map[string]bool{ + "DELETED": true, "EXACT": true, "SOFTSEEK": true, "EXCLUSIVE": true, + "FIXED": true, "CANCEL": true, "BELL": true, "CONFIRM": true, + "INSERT": true, "ESCAPE": true, "WRAP": true, "INTENSITY": true, + "SCOREBOARD": true, "CONSOLE": true, "ALTERNATE": true, "PRINTER": true, + } + + if booleanSets[upperSetting] { + if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { + extra = strings.ToUpper(p.expectMethodName().Literal) + } + } else if p.match(token.TO) { if upperSetting == "FILTER" || upperSetting == "RELATION" || upperSetting == "ORDER" || upperSetting == "INDEX" { if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { expr = p.parseExpr() @@ -1718,6 +1731,10 @@ func (p *Parser) parseSet() *ast.SetCmd { p.advance() extra = p.expectMethodName().Literal } + } else if upperSetting == "DATE" || upperSetting == "DECIMALS" || upperSetting == "EPOCH" { + if p.current.Kind != token.NEWLINE && p.current.Kind != token.EOF { + expr = p.parseExpr() + } } } diff --git a/compiler/parser/stmtreg.go b/compiler/parser/stmtreg.go index b6d13b2..3cf2383 100644 --- a/compiler/parser/stmtreg.go +++ b/compiler/parser/stmtreg.go @@ -241,10 +241,7 @@ func (p *Parser) stmtRecallPackZap() ast.Stmt { } func (p *Parser) stmtSet() ast.Stmt { - // SET command — skip to EOL (SET COLOR, SET FILTER, SET ORDER, etc.) - p.skipToEndOfLine() - p.expectEndOfStmt() - return &ast.ExprStmt{X: &ast.LiteralExpr{Kind: token.NIL_LIT, Value: "NIL"}} + return p.parseSet() } func (p *Parser) stmtDefer() ast.Stmt { return p.parseDefer() } diff --git a/examples/test_search.prg b/examples/test_search.prg new file mode 100644 index 0000000..b4c02ad --- /dev/null +++ b/examples/test_search.prg @@ -0,0 +1,229 @@ +// Comprehensive search test: SEEK, SOFTSEEK, SET DELETED, LOCATE/CONTINUE +// Tests all search-related RDD functionality end-to-end. +FUNCTION Main() + LOCAL i, aStruct, nCount + + ? "==========================================" + ? " Five Search/Filter Test Suite" + ? "==========================================" + ? + + // --- Setup: Create test table --- + aStruct := {{"ID","N",6,0}, {"NAME","C",20,0}, {"CITY","C",15,0}, {"AGE","N",3,0}, {"ACTIVE","L",1,0}} + dbCreate("search_test", aStruct) + USE "search_test" + + // Insert 50 records + FOR i := 1 TO 50 + APPEND BLANK + FieldPut(1, i) + FieldPut(2, "Name_" + PadL(LTrim(Str(i)), 3, "0")) + IF i % 5 == 0 + FieldPut(3, "Seoul") + ELSEIF i % 5 == 1 + FieldPut(3, "Tokyo") + ELSEIF i % 5 == 2 + FieldPut(3, "Beijing") + ELSEIF i % 5 == 3 + FieldPut(3, "London") + ELSE + FieldPut(3, "NewYork") + ENDIF + FieldPut(4, 20 + (i % 40)) + FieldPut(5, (i % 3 != 0)) + NEXT + + ? "Setup: 50 records created, RecCount =", RecCount() + ? + + // ============================================ + // TEST 1: INDEX + SEEK (exact match) + // ============================================ + ? "--- TEST 1: INDEX + SEEK ---" + INDEX ON NAME TO search_name + ? "Index on NAME created." + + SEEK "Name_001" + ? "SEEK 'Name_001': Found =", Found(), "RecNo =", RecNo() + IF !Found() + ? " *** FAIL: should find Name_001" + ENDIF + + SEEK "Name_025" + ? "SEEK 'Name_025': Found =", Found(), "RecNo =", RecNo() + IF !Found() + ? " *** FAIL: should find Name_025" + ENDIF + + SEEK "Name_050" + ? "SEEK 'Name_050': Found =", Found(), "RecNo =", RecNo() + IF !Found() + ? " *** FAIL: should find Name_050" + ENDIF + + SEEK "Name_999" + ? "SEEK 'Name_999': Found =", Found(), "Eof =", Eof() + IF Found() + ? " *** FAIL: should NOT find Name_999" + ENDIF + ? + + // ============================================ + // TEST 2: SOFTSEEK + // ============================================ + ? "--- TEST 2: SOFTSEEK ---" + SET SOFTSEEK ON + SEEK "Name_030" + ? "SOFTSEEK 'Name_030': Found =", Found(), "RecNo =", RecNo() + IF Found() + ? " Name =", FieldGet(2) + ENDIF + + SEEK "Name_031" + ? "SOFTSEEK 'Name_031': Found =", Found(), "RecNo =", RecNo() + IF !Found() .AND. !Eof() + ? " Positioned at:", FieldGet(2), "(softseek OK)" + ENDIF + + SET SOFTSEEK OFF + ? + + // ============================================ + // TEST 3: Partial key SEEK + // ============================================ + ? "--- TEST 3: Partial Key SEEK ---" + SEEK "Name_01" + ? "SEEK 'Name_01' (partial): Found =", Found() + IF Found() + ? " Name =", FieldGet(2), "(should be Name_010 or similar)" + ENDIF + ? + + // ============================================ + // TEST 4: SET DELETED — skip deleted records + // ============================================ + ? "--- TEST 4: SET DELETED ---" + + // Delete records 10,20,30,40,50 (every 10th) + SET ORDER TO 0 // natural order + GO TOP + FOR i := 1 TO 50 + GO i + IF FieldGet(1) % 10 == 0 + DELETE + ENDIF + NEXT + ? "Deleted records: 10, 20, 30, 40, 50" + + SET DELETED OFF + GO TOP + nCount := 0 + DO WHILE !Eof() + nCount++ + SKIP + ENDDO + ? "SET DELETED OFF: visible =", nCount, "(should be 50)" + + SET DELETED ON + GO TOP + nCount := 0 + DO WHILE !Eof() + nCount++ + SKIP + ENDDO + ? "SET DELETED ON: visible =", nCount, "(should be 45)" + + IF nCount != 45 + ? " *** FAIL: expected 45, got", nCount + ELSE + ? " PASS" + ENDIF + + // GoTop should skip deleted record if record 1 would be first + // Let's delete record 1 and test + GO 1 + DELETE + SET DELETED ON + GO TOP + ? "After deleting rec 1, GO TOP recNo =", RecNo(), "(should be 2)" + IF RecNo() != 2 + ? " *** FAIL: GoTop should skip deleted rec 1" + ELSE + ? " PASS" + ENDIF + + // Recall record 1 for subsequent tests + GO 1 + RECALL + SET DELETED OFF + ? + + // ============================================ + // TEST 5: SET DELETED + INDEX + // ============================================ + ? "--- TEST 5: SET DELETED + INDEX ---" + SET ORDER TO 1 // NAME index + SET DELETED ON + GO TOP + nCount := 0 + DO WHILE !Eof() + nCount++ + SKIP + ENDDO + ? "Indexed + SET DELETED ON: visible =", nCount, "(should be 45)" + IF nCount != 45 + ? " *** FAIL" + ELSE + ? " PASS" + ENDIF + SET DELETED OFF + ? + + // ============================================ + // TEST 6: Navigation correctness + // ============================================ + ? "--- TEST 6: Navigation ---" + SET ORDER TO 0 // natural + GO TOP + ? "GO TOP: RecNo =", RecNo(), "(should be 1)" + GO BOTTOM + ? "GO BOTTOM: RecNo =", RecNo(), "(should be 50)" + SKIP -1 + ? "SKIP -1: RecNo =", RecNo(), "(should be 49)" + GO TOP + SKIP 5 + ? "GO TOP + SKIP 5: RecNo =", RecNo(), "(should be 6)" + ? + + // ============================================ + // TEST 7: SEEK on city index + // ============================================ + ? "--- TEST 7: Second index ---" + INDEX ON CITY TO search_city + ? "Index on CITY created." + + SEEK "Seoul" + ? "SEEK 'Seoul': Found =", Found() + IF Found() + ? " City =", FieldGet(3), "ID =", FieldGet(1) + ENDIF + + SEEK "Tokyo" + ? "SEEK 'Tokyo': Found =", Found() + IF Found() + ? " City =", FieldGet(3), "ID =", FieldGet(1) + ENDIF + + SEEK "Paris" + ? "SEEK 'Paris': Found =", Found(), "(should be .F.)" + ? + + // ============================================ + // Cleanup + // ============================================ + CLOSE ALL + ? "==========================================" + ? " All search tests complete." + ? "==========================================" + +RETURN diff --git a/hbrdd/base.go b/hbrdd/base.go index 2ee5d37..965acb1 100644 --- a/hbrdd/base.go +++ b/hbrdd/base.go @@ -27,6 +27,10 @@ type BaseArea struct { filterExpr string filterBlock func(*hbrt.Thread) bool + // Locate + locateExpr string + locateBlock func(*hbrt.Thread) bool + // Relations relations []*Relation } @@ -120,3 +124,23 @@ func (a *BaseArea) ClearRelation() error { a.relations = nil return nil } + +// --- Locate support --- + +func (a *BaseArea) SetLocate(expr string, block func(*hbrt.Thread) bool) { + a.locateExpr = expr + a.locateBlock = block +} + +func (a *BaseArea) LocateBlock() func(*hbrt.Thread) bool { + return a.locateBlock +} + +// --- Global RDD configuration callbacks --- +// These are set by hbrtl to avoid circular imports. + +var ( + // IsSetDeleted returns true when SET DELETED is ON. + // Set by hbrtl.init() to hbrtl.GetSetDeleted. + IsSetDeleted func() bool +) diff --git a/hbrdd/dbf/dbf.go b/hbrdd/dbf/dbf.go index 30d08fc..6303c61 100644 --- a/hbrdd/dbf/dbf.go +++ b/hbrdd/dbf/dbf.go @@ -481,6 +481,10 @@ func (a *DBFArea) Skip(count int64) error { if err := a.GoTo(newRec); err != nil { return err } + // Skip deleted records when SET DELETED ON + if err := a.skipFilter(1); err != nil { + return err + } } } else { for i := int64(0); i > count; i-- { @@ -491,6 +495,9 @@ func (a *DBFArea) Skip(count int64) error { if err := a.GoTo(a.recNo - 1); err != nil { return err } + if err := a.skipFilter(-1); err != nil { + return err + } } } @@ -706,9 +713,49 @@ func (a *DBFArea) updateHeader() { WriteHeader(a.dataFile, &a.header) } +// skipFilter advances cursor past deleted/filtered records. +// Harbour: hb_dbfSkipFilter in dbf1.c +// direction: 1 = forward, -1 = backward func (a *DBFArea) skipFilter(direction int64) error { - // TODO: implement SET FILTER and SET DELETED filtering - // For now, no filtering applied + if direction == 0 { + direction = 1 + } + + setDel := hbrdd.IsSetDeleted != nil && hbrdd.IsSetDeleted() + + if !setDel { + return nil + } + + for !a.FEof && !a.FBof { + if !a.Deleted() { + break + } + + // Move to next/prev record + if direction > 0 { + newRec := a.recNo + 1 + if newRec > a.recCount { + a.FEof = true + a.recNo = a.recCount + 1 + break + } + if err := a.GoTo(newRec); err != nil { + return err + } + } else { + if a.recNo <= 1 { + a.FBof = true + if err := a.GoTo(1); err != nil { + return err + } + break + } + if err := a.GoTo(a.recNo - 1); err != nil { + return err + } + } + } return nil } diff --git a/hbrdd/driver.go b/hbrdd/driver.go index 60d8673..e22ffd4 100644 --- a/hbrdd/driver.go +++ b/hbrdd/driver.go @@ -125,6 +125,16 @@ type Area interface { // Bulk operations Pack() error Zap() error + + // State — Harbour: hb_waSetFound, locate support + SetFound(b bool) + SetLocate(expr string, block func(*hbrt.Thread) bool) + LocateBlock() func(*hbrt.Thread) bool + + // Filter + SetFilter(expr string, block func(*hbrt.Thread) bool) error + ClearFilter() error + HasFilter() bool } // --- Optional interfaces (drivers implement as needed) --- @@ -177,14 +187,6 @@ type Locker interface { IsLocked(recNo uint32) bool } -// Filterer provides SET FILTER support. -// Harbour: SELF_SETFILTER, SELF_CLEARFILTER -type Filterer interface { - SetFilter(expr string, block func(*hbrt.Thread) bool) error - ClearFilter() error - HasFilter() bool -} - // Relater provides SET RELATION support. // Harbour: SELF_SETREL, SELF_CLEARREL, SELF_FORCEREL type Relater interface { diff --git a/hbrdd/mem/memrdd.go b/hbrdd/mem/memrdd.go index 1b3dd9e..20ebfbc 100644 --- a/hbrdd/mem/memrdd.go +++ b/hbrdd/mem/memrdd.go @@ -131,6 +131,12 @@ type memArea struct { curIndex int // -1 = natural order, 0+ = index indexPos int // position in current index closed bool + + // Filter/Locate + filterExpr string + filterBlock func(*hbrt.Thread) bool + locateExpr string + locateBlock func(*hbrt.Thread) bool } func newMemArea(tbl *memTable, alias string, drv *MemDriver) *memArea { @@ -173,7 +179,26 @@ func (a *memArea) Flush() error { return nil } // no-op: memory only func (a *memArea) BOF() bool { return a.bof } func (a *memArea) EOF() bool { return a.eof } -func (a *memArea) Found() bool { return a.found } +func (a *memArea) Found() bool { return a.found } +func (a *memArea) SetFound(b bool) { a.found = b } + +func (a *memArea) SetLocate(expr string, block func(*hbrt.Thread) bool) { + a.locateExpr = expr + a.locateBlock = block +} +func (a *memArea) LocateBlock() func(*hbrt.Thread) bool { return a.locateBlock } + +func (a *memArea) SetFilter(expr string, block func(*hbrt.Thread) bool) error { + a.filterExpr = expr + a.filterBlock = block + return nil +} +func (a *memArea) ClearFilter() error { + a.filterExpr = "" + a.filterBlock = nil + return nil +} +func (a *memArea) HasFilter() bool { return a.filterBlock != nil } func (a *memArea) GoTo(recNo uint32) error { a.tbl.mu.RLock() diff --git a/hbrtl/database.go b/hbrtl/database.go index f6fec4a..6f77fd8 100644 --- a/hbrtl/database.go +++ b/hbrtl/database.go @@ -400,3 +400,203 @@ func rtlDbZap(t *hbrt.Thread) { } t.RetNil() } + +// --- LOCATE / CONTINUE --- +// Harbour: DBLOCATE(bFor, bWhile, nNext, nRec, lRest) → .T./.F. +// Searches from current position for record matching condition. + +func rtlDbLocate(t *hbrt.Thread) { + nParams := t.ParamCount() + t.Frame(nParams, 0) + defer t.EndProc() + + wam := getWA(t) + if wam == nil { + t.RetBool(false) + return + } + area := wam.Current() + if area == nil { + t.RetBool(false) + return + } + + // param 1: bFor condition block + var bFor hbrt.Value + if nParams >= 1 && !t.Local(1).IsNil() { + bFor = t.Local(1) + } + // param 2: bWhile condition block + var bWhile hbrt.Value + if nParams >= 2 && !t.Local(2).IsNil() { + bWhile = t.Local(2) + } + // param 3: nNext — max records to scan + nNext := int64(0) // 0 = all + if nParams >= 3 && !t.Local(3).IsNil() { + nNext = t.Local(3).AsNumInt() + } + // param 4: nRec — specific record number + if nParams >= 4 && !t.Local(4).IsNil() { + nRec := uint32(t.Local(4).AsNumInt()) + area.GoTo(nRec) + if bFor.IsBlock() { + t.PendingParams2(0) + bFor.AsBlock().Fn(t) + result := t.Pop2() + area.SetFound(result.AsBool()) + } else { + area.SetFound(true) + } + t.RetBool(area.Found()) + return + } + // param 5: lRest — .T. = continue from current, .F. = from top + lRest := false + if nParams >= 5 && !t.Local(5).IsNil() { + lRest = t.Local(5).AsBool() + } + + if !lRest && nNext == 0 { + area.GoTop() + } + + // Store locate block for __dbContinue + if bFor.IsBlock() { + area.SetLocate("", func(lt *hbrt.Thread) bool { + lt.PendingParams2(0) + bFor.AsBlock().Fn(lt) + return lt.Pop2().AsBool() + }) + } + + found := false + count := int64(0) + for !area.EOF() { + if nNext > 0 && count >= nNext { + break + } + // Check WHILE condition + if bWhile.IsBlock() { + t.PendingParams2(0) + bWhile.AsBlock().Fn(t) + if !t.Pop2().AsBool() { + break + } + } + // Check FOR condition + if bFor.IsBlock() { + t.PendingParams2(0) + bFor.AsBlock().Fn(t) + if t.Pop2().AsBool() { + found = true + break + } + } else { + found = true + break + } + area.Skip(1) + count++ + } + area.SetFound(found) + t.RetBool(found) +} + +// __DBCONTINUE — continue previous LOCATE search. +// Harbour: __dbContinue() +func rtlDbContinue(t *hbrt.Thread) { + t.Frame(0, 0) + defer t.EndProc() + + wam := getWA(t) + if wam == nil { + t.RetBool(false) + return + } + area := wam.Current() + if area == nil { + t.RetBool(false) + return + } + + blk := area.LocateBlock() + if blk == nil { + area.SetFound(false) + t.RetBool(false) + return + } + + // Skip past current record + area.Skip(1) + + found := false + for !area.EOF() { + if blk(t) { + found = true + break + } + area.Skip(1) + } + area.SetFound(found) + t.RetBool(found) +} + +// --- DBSETFILTER / DBCLEARFILTER / DBFILTER --- + +// DBSETFILTER(bCondition [, cCondition]) +func rtlDbSetFilter(t *hbrt.Thread) { + nParams := t.ParamCount() + t.Frame(nParams, 0) + defer t.EndProc() + + wam := getWA(t) + if wam == nil { + t.RetNil() + return + } + area := wam.Current() + if area == nil { + t.RetNil() + return + } + + if nParams >= 1 && t.Local(1).IsBlock() { + bFilter := t.Local(1) + expr := "" + if nParams >= 2 && t.Local(2).IsString() { + expr = t.Local(2).AsString() + } + area.SetFilter(expr, func(lt *hbrt.Thread) bool { + lt.PendingParams2(0) + bFilter.AsBlock().Fn(lt) + return lt.Pop2().AsBool() + }) + } + t.RetNil() +} + +// DBCLEARFILTER() +func rtlDbClearFilter(t *hbrt.Thread) { + t.Frame(0, 0) + defer t.EndProc() + + wam := getWA(t) + if wam == nil { + t.RetNil() + return + } + if area := wam.Current(); area != nil { + area.ClearFilter() + } + t.RetNil() +} + +// DBFILTER() → cFilterExpression +func rtlDbFilter(t *hbrt.Thread) { + t.Frame(0, 0) + defer t.EndProc() + + // TODO: return stored filter expression from area + t.RetString("") +} diff --git a/hbrtl/register.go b/hbrtl/register.go index c7a4771..27c9d91 100644 --- a/hbrtl/register.go +++ b/hbrtl/register.go @@ -179,6 +179,14 @@ func RegisterRTL(vm *hbrt.VM) { hbrt.Sym("PACK", hbrt.FsPublic, rtlDbPack), hbrt.Sym("ZAP", hbrt.FsPublic, rtlDbZap), + // Locate / Filter + hbrt.Sym("DBLOCATE", hbrt.FsPublic, rtlDbLocate), + hbrt.Sym("__DBLOCATE", hbrt.FsPublic, rtlDbLocate), + hbrt.Sym("__DBCONTINUE", hbrt.FsPublic, rtlDbContinue), + hbrt.Sym("DBSETFILTER", hbrt.FsPublic, rtlDbSetFilter), + hbrt.Sym("DBCLEARFILTER", hbrt.FsPublic, rtlDbClearFilter), + hbrt.Sym("DBFILTER", hbrt.FsPublic, rtlDbFilter), + // Encoding / Hashing (Go stdlib) hbrt.Sym("HB_MD5", hbrt.FsPublic, HbMD5), hbrt.Sym("HB_SHA256", hbrt.FsPublic, HbSHA256), @@ -247,6 +255,18 @@ func RegisterRTL(vm *hbrt.VM) { hbrt.Sym("__SETDATEFORMAT", hbrt.FsPublic, SetDateFunc), hbrt.Sym("__SETDECIMALS", hbrt.FsPublic, SetDecimalsFunc), hbrt.Sym("__SETEPOCH", hbrt.FsPublic, SetEpochFunc), + // SET toggle functions + hbrt.Sym("SETDELETED", hbrt.FsPublic, SetDeletedFunc), + hbrt.Sym("SETEXACT", hbrt.FsPublic, SetExactFunc), + hbrt.Sym("SETSOFTSEEK", hbrt.FsPublic, SetSoftSeekFunc), + hbrt.Sym("SETEXCLUSIVE", hbrt.FsPublic, SetExclusiveFunc), + hbrt.Sym("SETFIXED", hbrt.FsPublic, SetFixedFunc), + hbrt.Sym("SETCANCEL", hbrt.FsPublic, SetCancelFunc), + hbrt.Sym("SETBELL", hbrt.FsPublic, SetBellFunc), + hbrt.Sym("SETCONFIRM", hbrt.FsPublic, SetConfirmFunc), + hbrt.Sym("SETINSERT", hbrt.FsPublic, SetInsertFunc), + hbrt.Sym("SETESCAPE", hbrt.FsPublic, SetEscapeFunc), + hbrt.Sym("SETWRAP", hbrt.FsPublic, SetWrapFunc), // SET constants hbrt.Sym("_SET_EXACT", hbrt.FsPublic, SetConstExact), hbrt.Sym("_SET_DELETED", hbrt.FsPublic, SetConstDeleted), diff --git a/hbrtl/setcmd.go b/hbrtl/setcmd.go index 773d7c3..b6457e3 100644 --- a/hbrtl/setcmd.go +++ b/hbrtl/setcmd.go @@ -8,6 +8,7 @@ package hbrtl import ( + "five/hbrdd" "five/hbrt" "sync" ) @@ -261,8 +262,10 @@ func SetConstDateFmt(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.Pus func SetConstDecimals(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetDecimals); t.RetValue() } func SetConstEpoch(t *hbrt.Thread) { t.Frame(0, 0); defer t.EndProc(); t.PushInt(SetEpoch); t.RetValue() } -// init registers default DATE format and EPOCH +// init registers default DATE format and EPOCH, and hooks into hbrdd. func init() { + hbrdd.IsSetDeleted = GetSetDeleted + setMu.Lock() if _, ok := settings[SetDateFmt]; !ok { settings[SetDateFmt] = hbrt.MakeString("mm/dd/yy") diff --git a/hbrtl/strings.go b/hbrtl/strings.go index 0023771..fc18a48 100644 --- a/hbrtl/strings.go +++ b/hbrtl/strings.go @@ -253,28 +253,44 @@ func Space(t *hbrt.Thread) { // PadR pads a string on the right to a specified length. func PadR(t *hbrt.Thread) { - t.Frame(2, 0) + nParams := t.ParamCount() + t.Frame(nParams, 0) defer t.EndProc() s := valueToDisplay(t.Local(1)) n := int(t.Local(2).AsNumInt()) + fill := " " + if nParams >= 3 && t.Local(3).IsString() { + f := t.Local(3).AsString() + if len(f) > 0 { + fill = f[:1] + } + } if len(s) >= n { t.PushString(s[:n]) } else { - t.PushString(s + strings.Repeat(" ", n-len(s))) + t.PushString(s + strings.Repeat(fill, n-len(s))) } t.RetValue() } // PadL pads a string on the left to a specified length. func PadL(t *hbrt.Thread) { - t.Frame(2, 0) + nParams := t.ParamCount() + t.Frame(nParams, 0) defer t.EndProc() s := valueToDisplay(t.Local(1)) n := int(t.Local(2).AsNumInt()) + fill := " " + if nParams >= 3 && t.Local(3).IsString() { + f := t.Local(3).AsString() + if len(f) > 0 { + fill = f[:1] + } + } if len(s) >= n { t.PushString(s[len(s)-n:]) } else { - t.PushString(strings.Repeat(" ", n-len(s)) + s) + t.PushString(strings.Repeat(fill, n-len(s)) + s) } t.RetValue() }