Compare commits

..

No commits in common. "master" and "v1.0.0" have entirely different histories.

102 changed files with 3270 additions and 34771 deletions

3
.gitignore vendored
View File

@ -1,7 +1,4 @@
*.bak
*.lst
*.tmp
*.sh
*.log
.idea/
src/.idea/

8
.idea/.gitignore generated vendored Normal file
View File

@ -0,0 +1,8 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml

8
.idea/modules.xml generated Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/z80em.iml" filepath="$PROJECT_DIR$/.idea/z80em.iml" />
</modules>
</component>
</project>

28
LICENSE
View File

@ -1,28 +0,0 @@
BSD 3-Clause License
Copyright (c) 2026, Roman Boykov
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,88 +0,0 @@
# Эмулятор персонального компьютера Океан-240.2
![Icon](img/IconBig.png?raw=true "AppIcon")
## Зачем?
Эмулятор был разработан для удобства реверс-инжененринга программ монитора и приложений для этого старого компьютера.
Поэтому и имеет такой минималистичный интерфейс.
Если нужен более точный эмулятор Океана-240.2 и других компьютеров на безе процессора КР580ВМ80, рекомендую проект [Emu80](https://emu80.org/).
## Особенности реализации
- Микросхемы КР580ВВ55, ВИ53, ВИ51, ВТ59, КР1818ВГ93 эмулируются в степени, достаточной для работы операционной системы и монитора.
- В отличие от оригинала, который использует CPU КР580ВМ80, в эмуляторе использована эмуляция Z80.
Эмулятор Z80 у меня уже был написан, поэтому использовал его, возможно, позже, обрежу его до i8080.
Из за меньшего количества тактов у некоторых инструкций Z80, эмуляция работает несколько быстрее оригинала.
- Эмулируются 2 дисковода с дисками 720К, такой вариант используется с обазами ROM CP/M и Монитора R8.
- Работает под современными ОС Windows и Linux. Используется фреймворк [Fyne](https://fyne.io/), что позволяет скомпилировать код и под другие платформы, но я не пробовал.
- Поддерживает ZRCP - протокол отладки эмулятора [ZEsarUX](https://github.com/chernandezba/zesarux). Это позволяет использовать среду разработки VSCode с плагином DeZog
для отладки исходного кода прямо в эмуляторе.
## Возможности отладки
Работают все функции плагина Dezog и протокола: [документация плагина](https://github.com/maziac/DeZog/blob/main/documentation/Usage.md#remote-types).
- Выполнение кода по шагам, в том числе и назад (с ограничениями плагина DeZog)
- Просмотр и изменение памяти
- Просмотр и изменение значения регистров процессора
- Условные и безусловные брейкпоинты
- ASSERTION - остановка по несоблюдению указанного условия
- WPMEM - остановка при обращении к указанным ячейкам памяти (можно указать тип обращения r|w|rw)
- CodeCoverage - в коде, цветом помечаются строки, выполненные процессором.
![Debug](img/Debug.jpg?raw=true "Debug in VSCode")
Можно отлаживать код и без VSCode, подключившись телнетом к порту отладчика, по умолчанию это localhost:10001, далее, можно передавать отладчику команды.
Их список доступен по команде help.
telnet localhost 10001
### Список доступных команд отладчика
about Shows about message
clear-membreakpoints Clear all memory breakpoints
close-all-menus Close all visible dialogs
cpu-code-coverage Sets cpu code coverage parameters
cpu-history Runs cpu history actions
cpu-step Run single opcode cpu step
disable-breakpoint Disable specific breakpoint
disable-breakpoints Disable all breakpoints
disassemble Disassemble at address
enable-breakpoint Enable specific breakpoint
enable-breakpoints Enable breakpoints
enter-cpu-step Enter cpu step to step mode
evaluate Evaluate expression
exit-cpu-step Exit cpu step to step mode
extended-stack Sets extended stack parameters, which allows you to see what kind of values are in the stack
get-cpu-frequency Get cpu frequency in HZ
get-current-machine Returns current machine name
get-machines Returns list of emulated machines
get-membreakpoints Get memory breakpoints list
get-memory-pages Returns current state of memory pages
get-os Shows emulator operating system
get-registers Get CPU registers
get-tstates-partial Get the t-states partial counter
get-tstates Get the t-states counter
get-version Shows emulator version
hard-reset-cpu Hard resets the machine
help Shows help screen or command help
hexdump Dumps memory at address, showing hex and ascii
load-binary Load binary file "file" at address "addr" with length "len", on the current memory zone
quit Closes connection
read-memory Dumps memory at address
reset-tstates-partial Resets the t-states partial counter
run Run cpu when on cpu step mode
save-binary Save binary file "file" from address "addr" with length "len", from the current memory zone
set-breakpoint Sets a breakpoint at desired index entry with condition
set-breakpointaction Sets a breakpoint action at desired index entry
set-breakpointpasscount Set pass count for breakpoint
set-debug-settings Set debug settings on remote command protocol
set-machine Set machine
set-membreakpoint Sets a memory breakpoint starting at desired address entry for type
set-register Changes register value
snapshot-load Loads a snapshot
snapshot-save Saves a snapshot
write-memory Writes a sequence of bytes starting at desired address on memory
write-port Writes value at port

76
config/config.go Normal file
View File

@ -0,0 +1,76 @@
package config
import (
"os"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
const defaultMonitorFile = "okean240/MON_r6.BIN"
const defaultCPMFile = "okean240/CPM_r7.BIN"
type OkEmuConfig struct {
LogFile string `yaml:"logFile"`
LogLevel string `yaml:"logLevel"`
MonitorFile string `yaml:"monitorFile"`
CPMFile string `yaml:"cpmFile"`
}
var config *OkEmuConfig
//func usage() {
// fmt.Printf("Use: %s --config <file_path>.yml\n", filepath.Base(os.Args[0]))
// os.Exit(2)
//}
func GetConfig() *OkEmuConfig {
return config
}
func LoadConfig() {
//args := os.Args
//if len(args) != 3 {
// usage()
//}
//
//if args[1] != "--config" {
// usage()
//}
// confFile := args[2]
confFile := "okemu.yml"
conf := OkEmuConfig{}
data, err := os.ReadFile(confFile)
if err == nil {
err := yaml.Unmarshal(data, &conf)
if err != nil {
log.Panicf("Config file error: %v", err)
}
setDefaultConf(&conf)
checkConfig(&conf)
} else {
log.Panicf("Can not open config file: %v", err)
}
config = &conf
}
func checkConfig(conf *OkEmuConfig) {
}
func setDefaultConf(conf *OkEmuConfig) {
if conf.LogLevel == "" {
conf.LogLevel = "info"
}
if conf.LogFile == "" {
conf.LogFile = "okemu.log"
}
if conf.MonitorFile == "" {
conf.MonitorFile = defaultMonitorFile
}
if conf.CPMFile == "" {
conf.CPMFile = defaultCPMFile
}
}

View File

@ -1,109 +0,0 @@
# Эмулятор персонального компьютера Океан-240.2
## Пример, кода для работы в среде VSCode.
1. Установите VSCode
2. Установите плагин DeZog для возможности отладки исходного кода в эмуляторе okemu
3. Откройте в VSCode папку examples/hello
4. Запустите эмулятор okemu
5. Запустите отладку через меню Run -> Start debuging в VSCode.
6. Используйте инструменты отладки VSCode для выполнения кода, просмотра регистров процессора и состояния памяти и т.д.
## Рекомендуемые плагины VSCode
- [mborik.z80-macroasm-vscode](https://github.com/mborik/z80-macroasm-vscode) - Раскраска синтаксиса ассемблера Z80, поддержка разных ассемблеров, переименование меток и т.п.
- [maziac.dezog](https://github.com/maziac/DeZog/) - Отладчик кода Z80 с помощью эмуляторов
- [maziac.hex-hover-converter](https://github.com/maziac/hex-hover-converter) - Перевод чисел в другие системы счисления при наведении курсора
- [maziac.z80-instruction-set](https://github.com/maziac/z80-instruction-set) - Подсказки по инструкциям Z80 при наведении курсора (коды, такты, влияние на флаги)
## Пример файла .vscode/tasks.json
Конфигурация задачи для компиляции исходного кода ассемблером [sjasmplus](https://github.com/z00m128/sjasmplus)
```json
{
"version": "2.0.0",
"tasks": [
{
"label": "make (sjasmplus)", // так задача будет называться в меню Terminal -> Run task... VSCode
"type": "shell",
"command": "sjasmplus",
"args": [
"--i8080", // используем только инструкции КР580ВМ80
"--sld=main.sld", // генерируем файл с таблицей символов для отладчика
"--raw=main.obj", // сохраняем результат компиляции в двоичный файл
"--fullpath",
"main.asm" // основной файл исходного кода программы
],
"problemMatcher": "$problem-matcher-sjasmplus",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
```
## Пример файла .vscode/launch.json
Конфигурация задачи для отладки исходного кода нашей программы с помощью эмулятора okemu. Эмулятор поддерживает подмножество протокола ZRCP (протокол отладки эмулятора [ZEsarUX](https://github.com/chernandezba/zesarux)).
Для отладки нужен плагин DeZog. Подробное описание настроек в [документации плагина](https://github.com/maziac/DeZog/blob/main/documentation/Usage.md#remote-types).
```json
{
"version": "0.2.0",
"configurations": [
{
"type": "dezog",
"request": "launch",
"name": "Simulator",
"remoteType": "zrcp", // используем протокол ZRCP для соединения с okemu
"zrcp": {
"port": 10001, // порт, на котором, по умолчанию, отвечает отладчик okemu
// если эмулятор запущен на другом компьютере, нужно указать его имя или адрес в параметре "host"
},
"sjasmplus": [
{
"path": "main.sld", // файл с символами для отладки, созданный при компиляции
},
// ниже, исходники и файлы для отладки ОС и монитора Океана, если мы хотим видеть их исходный в VSCode в процессе отладки
// их можно убрать. Тогда при трассировке вызовов ОС и монитора мы будем видеть дизасм кода.
{
"path": "cpm/cpm.sld",
"srcDirs": [
"cpm/"
]
},
{
"path": "mon/monitor.sld",
"srcDirs": [
"mon/"
]
}
],
"smartDisassemblerArgs": {
"lowerCase": false
},
"history": {
"reverseDebugInstructionCount": 20, // количество инструкций, которые можно шагнуть назад
"spotCount": 10,
"codeCoverageEnabled": true //true Если мы хотим видеть, какие участки кода нашего приложения выполнялись
},
"startAutomatically": false,
"commandsAfterLaunch": [
//"-rmv", // открыть окно с памятью, на которую указывают 16-разрядные регистры BC,DE,HL,IX,IY
"-mv 0x100 0x80" // открыть окно для просмотра памяти с адреса 100h размером 80h байт
],
"rootFolder": "${workspaceFolder}",
"topOfStack": "stack",
"loadObjs": [
{
"path": "main.obj", // Этот файл будет загружен в эмулятор
"start": "0x0100" // Файл будет загружен с адреса 100h
}
],
"execAddress": "0x0100", // Загруженный код начнет выполняться с адреса 100h
"smallValuesMaximum": 513,
"tmpDir": ".tmp"
}
]
}
```

View File

@ -1,9 +0,0 @@
{
"recommendations": [
"maziac.asm-code-lens",
"maziac.dezog",
"maziac.hex-hover-converter",
"maziac.z80-instruction-set",
"maziac.sna-fileviewer"
]
}

View File

@ -1,60 +0,0 @@
{
"version": "0.2.0",
"configurations": [
{
"type": "dezog",
"request": "launch",
"name": "Simulator",
"remoteType": "zrcp",
"zrcp": {
"port": 10001,
},
"sjasmplus": [
{
"path": "main.sld",
},
{
"path": "cpm/cpm.sld",
"srcDirs": [
"cpm/"
]
},
{
"path": "mon/monitor.sld",
"srcDirs": [
"mon/"
]
}
],
"smartDisassemblerArgs": {
"lowerCase": false
},
// "disassemblerArgs": {
// "numberOfLines": 10
// },
"history": {
"reverseDebugInstructionCount": 20,
"spotCount": 10,
"codeCoverageEnabled": true
},
"startAutomatically": false,
"commandsAfterLaunch": [
//"-rmv",
"-mv 0x100 0x80"
],
"rootFolder": "${workspaceFolder}",
"topOfStack": "stack",
"loadObjs": [
{
"path": "main.obj",
"start": "0x0100"
}
],
"execAddress": "0x0100",
"smallValuesMaximum": 513,
"tmpDir": ".tmp"
}
]
}

View File

@ -1,3 +0,0 @@
{
"mcu-debug.debug-tracker-vscode.debugLevel": 1
}

View File

@ -1,23 +0,0 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "make (sjasmplus)",
"type": "shell",
"command": "sjasmplus",
"args": [
"--i8080",
"--sld=main.sld",
"--raw=main.obj",
"--fullpath",
"--lst",
"main.asm"
],
"problemMatcher": "$problem-matcher-sjasmplus",
"group": {
"kind": "build",
"isDefault": true
}
}
]
}

View File

@ -1,7 +0,0 @@
# Эмулятор персонального компьютера Океан-240.2
Простой пример программы для ОС CP/M.
В папке .vscode прилагаются файлы, необходимые для настройки среды VSCode.
Для компиляции используется [sjasmplus Z80 assembler](https://github.com/z00m128/sjasmplus).

File diff suppressed because it is too large Load Diff

View File

@ -1,832 +0,0 @@
; =======================================================
; Ocean-240.2
;
; CP/M BIOS
;
; Disassembled by Romych 2025-09-09
; =======================================================
INCLUDE "equates.inc"
INCLUDE "ram.inc"
INCLUDE "mon_entries.inc"
DEFINE CHECK_INTEGRITY
IFNDEF BUILD_ROM
OUTPUT bios.bin
ENDIF
MODULE BIOS
ORG 0xD600
; -------------------------------------------------------
; BIOS JUMP TABLE
; -------------------------------------------------------
boot_f:
JP bios_boot
wboot_f:
JP bios_wboot
; -------------------------------------------------------
; console status to reg-a
; -------------------------------------------------------
const_f:
JP MON.non_con_status
; -------------------------------------------------------
; console character to reg-a
; -------------------------------------------------------
conin_f:
JP MON.mon_con_in
; -------------------------------------------------------
; console character from c to console out
; -------------------------------------------------------
conout_f:
JP MON.mon_con_out
; -------------------------------------------------------
; list device out
; -------------------------------------------------------
list_f:
JP MON.mon_char_print
; -------------------------------------------------------
; punch device out
; -------------------------------------------------------
punch_f:
JP MON.mon_serial_out
; -------------------------------------------------------
; reader character in to reg-a
; -------------------------------------------------------
reader_f:
JP MON.mon_serial_in
; -------------------------------------------------------
; move to home position, treat as track 00 seek
; -------------------------------------------------------
home_f:
JP home
; -------------------------------------------------------
; select disk given by register c
; -------------------------------------------------------
seldsk_f:
JP sel_disk
settrk_f:
JP set_trk
setsec_f:
JP set_sec
; -------------------------------------------------------
; Set DMA address from BC
; -------------------------------------------------------
setdma_f:
JP set_dma
read_f:
JP read
write_f:
JP write
listst_f:
JP list_st
sectran_f:
JP sec_tran
; -------------------------------------------------------
; Reserved
; -------------------------------------------------------
JP warm_boot
JP warm_boot
; -------------------------------------------------------
; Tape read
; -------------------------------------------------------
tape_read_f:
JP MON.mon_tape_read
; -------------------------------------------------------
; Tape write
; -------------------------------------------------------
tape_write_f:
JP MON.mon_tape_write
; -------------------------------------------------------
; Tape wait block
; -------------------------------------------------------
tape_wait_f:
JP MON.mon_tape_wait
JP warm_boot ; r8
disk_a_size DW 192 ; 192k disk A size
disk_b_size DW 720 ; 720 disk B size
disk_c_size DW 720 ; 720 disk C size
disk_b_tracks DB 80
disk_c_tracks DB 80
; -------------------------------------------------------
; cold start
; -------------------------------------------------------
bios_boot:
LD HL, (bdos_ent_addr) ; 0xba06
LD DE, 0x10000-CCP_RAM.bdos_enter_jump ; 0x45fa
ADD HL, DE ; 0xba06+0x45fa=10000
LD A, H
OR L
JP Z, bios_signon
; >> r8
LD HL, (bios_var0) ; if bios_var0 = 0xaaaa (initialized)
LD DE, 0x5556
ADD HL, DE ; 0xaaaa+0x5556=0x10000 if initialized
LD A, H
OR L
JP Z, bios_signon ; if initialized, go to logon, skip init
; <<
; Init DMA buffer
LD HL, dma_buffer ; 0x8000
LD B, DMA_BUFF_SIZE ; 0x80
.init_dma_buff:
LD (HL), FILE_DELETED ; 0xE5
INC HL
DEC B
JP NZ, .init_dma_buff
; Init RAM disk
LD HL, dma_buffer
LD DE, 0x0000
LD B, 8
.init_ram_dsk:
PUSH BC
CALL MON.ram_disk_write
POP BC
INC DE
DEC B
JP NZ, .init_ram_dsk
; Init user to 0 and drive to 0
XOR A
LD (cur_user_drv), A
; init bios variables
CALL bios_init_ilv ; r8
bios_signon:
LD SP, bios_stack
; Print CP/M hello message
LD HL, msg_hello
CALL print_strz
JP bios_wboot
;bios_wboot:
; LD SP, bios_stack
; r8 >>
bios_init_ilv:
LD HL, bios_var0
LD DE, bios_ini_vals
LD C, 13
CALL mov_dlhe_c
LD A, (disk_c_tracks) ; 0x50
LD (disk_c_tracks), A
RET
; <<
; -------------------------------------------------------
; BIOS Warm start entry
; -------------------------------------------------------
bios_wboot: ; r8
LD HL, (bios_var0)
LD DE, 0x5556 ; 0xaaaa + 0x5556 = 0x10000
ADD HL, DE
LD A, H
OR L
CALL NZ, bios_init_ilv ; init if not initialized before
LD SP, bios_stack
; Move CPP from 0xC000 to 0xB200
LD HL, CCP_DST_ADDR
LD DE, CCP_SRC_ADDR
LD BC, CCP_SIZE
.move_ccp:
LD A, (DE)
LD (HL), A
INC DE
INC HL
DEC BC
LD A, B
OR C
JP NZ, .move_ccp
; Init variables with zeroes
LD HL, CPM_VARS.cpm_vars_start
LD BC, CPM_VARS.ccp_vars_size ; 213
.clr_cpm_vars:
LD (HL), 0x0
INC HL
DEC BC
LD A, B
OR C
JP NZ, .clr_cpm_vars
LD A, FILE_DELETED
LD (CPM_VARS.bdos_efcb), A ; mark empty FCB
LD A, low dma_buffer ; 0x80
LD (CPM_VARS.bdos_dmaad), A ; 0x0080
; Move DPH + DPB to RAM
LD HL, CPM_VARS.DPH_RAM
LD DE, dph_disk_a
LD BC, DPB_END-dph_disk_a ; 0x39 -> 57d
.move_dph:
LD A, (DE)
LD (HL), A
INC HL
INC DE
DEC BC
LD A, B
OR C
JP NZ, .move_dph
LD BC, dma_buffer ; DMA default buffer addr
CALL setdma_f
; Setup JP to Warm boot after CPU reset
LD A, JP_OPCODE
LD (warm_boot), A
LD HL, wboot_f
LD (warm_boot_addr), HL
; Setup JP to BDOS entrance
LD (jp_bdos_enter), A
LD HL, CCP_RAM.bdos_enter_jump
LD (bdos_ent_addr), HL
; Disk A
LD HL, CPM_VARS.DPB_A_RAM
LD C, 0xf
LD DE, dpb_ram
LD A, (disk_a_size+1)
OR A
JP Z, .drv_a_192
LD DE, dpb_empty
.drv_a_192:
CALL mov_dlhe_c
; Disk B
LD HL, CPM_VARS.DPB_B_RAM
LD C, 0xf
LD DE, dpb_flop_360k
LD A, (disk_b_size+1)
CP 0x1
JP Z, .drv_b_360
LD DE, dpb_flop_720k
.drv_b_360:
CALL mov_dlhe_c
; Disk C
LD HL, CPM_VARS.DPB_C_RAM
LD C, 0xf
LD A, (disk_c_size+1)
CP 0x2 ; 720?
JP Z, .drv_c_720
LD DE, dpb_flop_360k
; bios_var3 != 0 -> move DPB 360k
LD A, (disk_sw_trk)
OR A
JP NZ, .drv_c_mov
; bios_var3 == 0 -> move DPB 720k
LD DE, dpb_flop_720k
JP .drv_c_mov
.drv_c_720:
LD DE, dpb_flop_720k
; bios_var3 != 0 -> move DPB 720k
LD A, (disk_sw_trk)
OR A
JP NZ, .drv_c_mov
; bios_var3 == 1 -> move DPB 360k
LD DE, dpb_flop_360k
.drv_c_mov:
CALL mov_dlhe_c
XOR A
LD (CPM_VARS.slicer_has_data),A
LD (CPM_VARS.slicer_uninited_count),A
LD A,(cur_user_drv)
LD C,A
; go to CCP
JP CCP_DST_ADDR
; -------------------------------------------------------
; Move C bytes from [DE] to [HL]
; -------------------------------------------------------
mov_dlhe_c:
LD A,(DE) ; [DE]->[HL]
LD (HL),A
INC HL
INC DE
DEC C
JP NZ, mov_dlhe_c
RET
list_st:
XOR A
RET
; -------------------------------------------------------
; Select disk
; Inp: C - disk,
; E - active drive flag
; Out: HL -> DPH
; -------------------------------------------------------
sel_disk:
LD HL, 0x00
LD A, C
CP CTRL_C
RET NC
LD (CPM_VARS.cur_disk), A
OR A
JP Z, .get_dph_addr ; skip next for disk 0 - RAM disk
LD A, E ; selected disk map
AND 0x1 ; bit 0 is set if disk already selected
JP NZ, .get_dph_addr
; Reset disk
LD (CPM_VARS.slicer_has_data), A
LD (CPM_VARS.slicer_uninited_count), A
; calc DPH address of new drive
.get_dph_addr:
LD L, C
LD H, 0x0
ADD HL, HL ; *2
ADD HL, HL ; *4
ADD HL, HL ; *8
ADD HL, HL ; *16 (size of DBH)
LD DE, CPM_VARS.DPH_RAM
ADD HL, DE
RET
; -------------------------------------------------------
; move to track 00
; -------------------------------------------------------
home:
LD A, (CPM_VARS.cur_disk)
OR A
JP Z, .is_default
LD A, (CPM_VARS.slicer_need_save)
OR A
JP NZ, .is_default
LD (CPM_VARS.slicer_has_data), A ; set to 0, no data
.is_default:
LD C, 0 ; set track to 0
; -------------------------------------------------------
; set track address (0..76) for subsequent read/write
; -------------------------------------------------------
set_trk:
LD HL, CPM_VARS.curr_track
LD (HL), C
RET
; -------------------------------------------------------
; set sector address (1,..., 26) for subsequent read/write
; -------------------------------------------------------
set_sec:
LD HL, CPM_VARS.curr_sec
LD (HL), C
RET
; -------------------------------------------------------
; set subsequent dma address (initially 80h)
; -------------------------------------------------------
set_dma:
LD L, C
LD H, B
LD (CPM_VARS.dma_addr), HL
RET
sec_tran:
LD L, C
LD H, B
RET
; -------------------------------------------------------
; read track/sector to preset dma address
; -------------------------------------------------------
read:
LD A, (CPM_VARS.cur_disk)
OR A
JP NZ, read_phys ; for physical disk use special routine
CALL ram_disk_calc_addr
CALL MON.ram_disk_read
XOR A
RET
; -------------------------------------------------------
; write track/sector from preset dma address
; -------------------------------------------------------
write:
LD A, (CPM_VARS.cur_disk)
OR A
JP NZ,write_phys
CALL ram_disk_calc_addr
CALL MON.ram_disk_write
XOR A
RET
; -------------------------------------------------------
; Calculate address for current sector and track
; -------------------------------------------------------
ram_disk_calc_addr:
LD HL, CPM_VARS.curr_track
; HL = cur_track * 16
LD L, (HL)
LD H, 0x0
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, HL
; DE = HL + cur_sec
EX DE, HL
LD HL, CPM_VARS.curr_sec
LD L, (HL)
LD H, 0x0
ADD HL, DE
EX DE, HL
; store address
LD HL, (CPM_VARS.dma_addr)
RET
read_phys:
CALL read_phys_op
RET
write_phys:
CALL write_phys_op
RET
read_phys_op:
XOR A
; reset counter
LD (CPM_VARS.slicer_uninited_count), A
LD A, 0x1
LD (CPM_VARS.tmp_slicer_operation), A ; 0 - write; 1 - read
LD (CPM_VARS.tmp_slicer_can_read), A ; enable read fron disk
LD A, 0x2
LD (CPM_VARS.tmp_slicer_flush), A ; disable flush data to disk
JP base_read_write
write_phys_op:
XOR A
LD (CPM_VARS.tmp_slicer_operation), A
LD A, C
LD (CPM_VARS.tmp_slicer_flush), A
CP 0x2
JP NZ, .mode_ne_2
LD A, 0x10 ; 2048/128
LD (CPM_VARS.slicer_uninited_count), A
LD A, (CPM_VARS.cur_disk)
LD (CPM_VARS.slicer_uninited_disk), A
LD A, (CPM_VARS.curr_track)
LD (CPM_VARS.slicer_uninited_track), A
LD A, (CPM_VARS.curr_sec)
LD (CPM_VARS.slicer_uninited_sector_128), A
.mode_ne_2:
LD A, (CPM_VARS.slicer_uninited_count)
OR A
JP Z, slicer_read_write
DEC A
LD (CPM_VARS.slicer_uninited_count), A
LD A, (CPM_VARS.cur_disk)
LD HL, CPM_VARS.slicer_uninited_disk
CP (HL)
JP NZ, slicer_read_write
LD A, (CPM_VARS.curr_track)
LD HL, CPM_VARS.slicer_uninited_track
CP (HL)
JP NZ, slicer_read_write
LD A, (CPM_VARS.curr_sec)
LD HL, CPM_VARS.slicer_uninited_sector_128
CP (HL)
JP NZ, slicer_read_write
INC (HL)
LD A, (HL)
CP 36 ; Sectors per track
JP C, .no_inc_track
LD (HL), 0x0
LD A, (CPM_VARS.slicer_uninited_track)
INC A
LD (CPM_VARS.slicer_uninited_track), A
.no_inc_track:
XOR A
LD (CPM_VARS.tmp_slicer_can_read), A
JP base_read_write
; --------------------------------------------------
slicer_read_write:
XOR A
LD (CPM_VARS.slicer_uninited_count), A
INC A
LD (CPM_VARS.tmp_slicer_can_read), A
; --------------------------------------------------
base_read_write:
XOR A
LD (CPM_VARS.tmp_slicer_result), A
LD A, (CPM_VARS.curr_sec)
OR A
RRA
OR A
RRA
LD (CPM_VARS.tmp_slicer_real_sector), A
LD HL, CPM_VARS.slicer_has_data
LD A, (HL)
LD (HL), 0x1
OR A
JP Z, .no_data
LD A, (CPM_VARS.cur_disk)
LD HL, CPM_VARS.slicer_disk
CP (HL)
JP NZ, .pos_diff
LD A, (CPM_VARS.curr_track)
LD HL, CPM_VARS.slicer_track
CP (HL)
JP NZ, .pos_diff
LD A, (CPM_VARS.tmp_slicer_real_sector)
LD HL, CPM_VARS.slicer_real_sector
CP (HL)
JP Z,calc_sec_addr_in_bfr
.pos_diff:
LD A, (CPM_VARS.slicer_need_save)
OR A
CALL NZ, slicer_save_buffer ; save buffer if needed
.no_data:
LD A, (CPM_VARS.cur_disk)
LD (CPM_VARS.slicer_disk), A
LD A, (CPM_VARS.curr_track)
LD (CPM_VARS.slicer_track), A
LD A, (CPM_VARS.tmp_slicer_real_sector)
LD (CPM_VARS.slicer_real_sector), A
LD A, (CPM_VARS.tmp_slicer_can_read)
OR A
CALL NZ,slicer_read_buffer
XOR A
LD (CPM_VARS.slicer_need_save), A
; --------------------------------------------------
calc_sec_addr_in_bfr:
LD A, (CPM_VARS.curr_sec)
AND 0x3
LD L, A
LD H, 0
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, HL
ADD HL, HL ; *128
LD DE, CPM_VARS.slicer_buffer
ADD HL, DE
EX DE, HL
LD HL, (CPM_VARS.dma_addr)
LD C, 0x80
LD A, (CPM_VARS.tmp_slicer_operation)
OR A
JP NZ, .no_save
LD A, 0x1
LD (CPM_VARS.slicer_need_save), A
EX DE, HL
.no_save:
LD A, (DE)
INC DE
LD (HL), A
INC HL
DEC C
JP NZ, .no_save
LD A, (CPM_VARS.tmp_slicer_flush)
CP 0x1
LD A, (CPM_VARS.tmp_slicer_result)
RET NZ
OR A
RET NZ
XOR A
LD (CPM_VARS.slicer_need_save), A
CALL slicer_save_buffer
LD A, (CPM_VARS.tmp_slicer_result)
RET
; --------------------------------------------------
slicer_save_buffer:
CALL slicer_get_floppy_args
LD C, 0xA6 ; VG93 CMD
CALL MON.write_floppy
LD (CPM_VARS.tmp_slicer_result), A
RET
; --------------------------------------------------
slicer_read_buffer:
CALL slicer_get_floppy_args
LD C, 0x86 ; VG93 CMD
CALL MON.read_floppy
LD (CPM_VARS.tmp_slicer_result), A
RET
; --------------------------------------------------
slicer_get_floppy_args:
LD HL, CPM_VARS.tmp_buff9
LD A, (CPM_VARS.slicer_disk)
DEC A
JP Z, .non_interleave
LD HL, interleave_0
.non_interleave:
LD A, (CPM_VARS.slicer_real_sector)
ADD A, L
LD L, A
LD E, (HL)
LD A, (CPM_VARS.slicer_track)
LD D, A
LD HL, CPM_VARS.slicer_buffer
LD A, (CPM_VARS.slicer_disk)
RET
; -------------------------------------------------------
; Print zerro ended string
; Inp: HL -> string
; -------------------------------------------------------
print_strz:
LD A, (HL)
OR A
RET Z
LD C, A
PUSH HL
CALL conout_f
POP HL
INC HL
JP print_strz
msg_hello:
DB ASCII_ESC, "6", "0" ; 40x25 cursor on
DB ASCII_ESC, "8", "3" ; set palette
DB ASCII_ESC, "5", 33, 37 ; set cursor r,c
DB ASCII_ESC, "4", "1" ; set color
DB ASCII_ESC, "1", 22, 226, 226, 252, 1 ; draw fill rect x1,y1,x2,y2,m
DB ASCII_ESC, "4", "0" ; set color
DB ASCII_ESC, "1", 30, 230, 219, 248, 1 ; draw fill rect x1,y1,x2,y2,m
DB ASCII_ESC, "4", "3" ; set color
DB "OKEAH-240 CP/M (V2.2) REL.8'\r\n\n"
DB ASCII_ESC, "4", "2", 0 ; set color
; --------------------------------------------------
; Disk parameters blocks in ROM (DPBs)
; Tables of memory that describe the characteristics
; of discs on our system. There is one DPB for each
; disc type
; ----------------------------------------
; Block size | No of sectors | BSH | BLM |
; ----------------------------------------
; 1K | 8 | 3 | 7 |
; 2K | 16 | 4 | 15 |
; 4K | 32 | 5 | 31 |
; 8K | 64 | 6 | 63 |
; 16K | 128 | 7 | 127 |
; ----------------------------------------
; -------------------------------------
; Block size| Extent mask (EXM) |
; | Small disk | Large disk |
; -------------------------------------
; 2K | 1 | 0 |
; 4K | 3 | 1 |
; 8K | 7 | 3 |
; 16K | 15 | 7 |
; -------------------------------------
; CKS - number of dir sectors to check before write, 0 for HDD
; For RAM-Disk 192k
dpb_ram:
DW 16 ; SPT Sector (128b) per track (16d)
DB 3 ; BSH 1k
DB 7 ; BLM 1k; Allocation block size = (BLM + 1) * 128 = 1k
DB 0 ; EXM extent mask
DW 191 ; DSM Disk size blocks - 1
DW 31 ; DRM Dir elements - 1
DB 10000000b ; AL0 Dir map byte 1
DB 00000000b ; AL1 Dir map byte 2
DW 0x0008 ; CKS checksum vector size (8 sectors=1k)
DW 0x0000 ; OFF (tracks reserved for system)
dpb_empty:
DS 15, 0xff
; For FLOPPY 720k
dpb_flop_720k:
DW 36 ; SPT Sector (128b) per track 36 * 128 = 18KB
DB 4 ; BSH 2k
DB 15 ; BLM 2k; Allocation block size = (BLM + 1) * 128 = 2k
DB 0 ; EXM extent mask
DW 359 ; DSM Disk size blocks - 1 (359d)
DW 127 ; DRM Directory entries - 1 (127d)
DB 11000000b ; AL0 Dir map byte 1 (2 dir blk)
DB 00000000b ; AL1 Dir map byte 2
DW 32 ; CKS checksum vector size (32 sectors = 2k)
DW 0x0000 ; OFF (No of tracks reserved for system)
; For FLOPPY 360k
dpb_flop_360k:
DW 36 ; SPT Sector (128b) per track 36 * 128 = 18KB
DB 4 ; BSH 2k
DB 15 ; BLM 2k; Allocation block size = (BLM + 1) * 128 = 2k
DB 1 ; EXM extent mask
DW 179 ; DSM Disk size blocks - 1 (179d)
DW 127 ; DRM Directory entries - 1 (127d)
DB 11000000b ; AL0 Dir map byte 1 (2 dir blk)
DB 00000000b ; AL1 Dir map byte 2
DW 32 ; CKS checksum vector size (32 sectors = 2k)
DW 0x0000 ; OFF (No of tracks reserved for system)
bios_ini_vals:
DB 0xaa, 0xaa, 0, 0xff, 1, 8, 6, 4, 2, 9, 7, 5, 3
; --------------------------------------------------
; Disk parameters headers in ROM
; --------------------------------------------------
; Disk A RAM
dph_disk_a:
DW 0 ; Sector translate table pointer
DW 0, 0, 0 ; Scratchpad area
DW CPM_VARS.dir_buffer ; Directory buffer pointer
DW CPM_VARS.DPB_A_RAM ; DPB Pointer
DW CPM_VARS.CHK_VEC_A ; Check Vector pointer
DW CPM_VARS.AL_MAP_A ; Allocation map pointer
; Disk B Floppy
dph_disk_b:
DW 0 ; Sector translate table pointer
DW 0, 0, 0 ; Scratchpad area
DW CPM_VARS.dir_buffer ; Directory buffer pointer
DW CPM_VARS.DPB_B_RAM ; DPB Pointer
DW CPM_VARS.CHK_VEC_B ; Check Vector pointer
DW CPM_VARS.AL_MAP_B ; Allocation map pointer
; Disk C Floppy
dph_disk_c:
DW 0 ; Sector translate table pointer
DW 0, 0, 0 ; Scratchpad area
DW CPM_VARS.dir_buffer ; Directory buffer pointer
DW CPM_VARS.DPB_C_RAM ; DPB Pointer
DW CPM_VARS.CHK_VEC_C ; Check Vector pointer
DW CPM_VARS.AL_MAP_C ; Allocation map pointer
res_data: ; offset 0xda28
DB 1, 8, 6, 4, 2, 9, 7, 5, 3
DPB_END EQU $
DB 0x0e, 3
; -------------------------------------------------------
; Filler to align blocks in ROM
; -------------------------------------------------------
LAST EQU $
CODE_SIZE EQU LAST-0xD600
FILL_SIZE EQU 0x500-CODE_SIZE
DISPLAY "| BIOS\t| ",/H,boot_f," | ",/H,CODE_SIZE," | ",/H,FILL_SIZE," |"
IFDEF CHECK_INTEGRITY
; Check integrity
ASSERT bios_wboot = 0xd6a8
ASSERT sel_disk = 0xd781
ASSERT home = 0xd7a7
ASSERT ram_disk_calc_addr = 0xd7eb
ASSERT write_phys_op = 0xd81e
ASSERT base_read_write = 0xd88a
ASSERT calc_sec_addr_in_bfr = 0xd8e4
ASSERT slicer_save_buffer = 0xd928
ASSERT print_strz = 0xd95e
ASSERT msg_hello = 0xd96b
ASSERT dpb_ram = 0xd9af
ASSERT dph_disk_a = 0xd9f8
ASSERT res_data = 0xda28
ENDIF
FILLER:
DS FILL_SIZE, 0xff
ENDMODULE
IFNDEF BUILD_ROM
OUTEND
ENDIF

File diff suppressed because it is too large Load Diff

View File

@ -1,882 +0,0 @@
; =======================================================
; Ocean-240.2
; CPM CPP, ROM PART
; AT 0xDB00
;
; Disassembled by Romych 2025-09-09
; =======================================================
INCLUDE "equates.inc"
INCLUDE "ram.inc"
IFNDEF BUILD_ROM
OUTPUT ccp_rom.bin
ENDIF
MODULE CCP_ROM
ORG 0xDB00
@ccp_entry:
LD HL, 0x0 ; prevent stack overflow
ADD HL, SP
LD (CPM_VARS.saved_stack_ptr), HL
LD SP, CPM_VARS.ccp_safe_stack
CALL get_cmd_index
LD HL, ccp_commands ;= DB6Ch
LD E, A
LD D, 0x0
ADD HL, DE
ADD HL, DE
LD A, (HL)
INC HL
LD H, (HL)
LD L, A
JP (HL) ; jump to command
ccp_commands_str:
DB "SDIR READ WRITE"
; -------------------------------------------------------
; Search user command position in available commands list
; -------------------------------------------------------
get_cmd_index:
LD HL, ccp_commands_str ; -> 'DIR'
LD C, 0x0
.cmd_next:
LD A, C
CP CCP_COMMAND_COUNT
RET NC
LD DE, CCP_RAM.ccp_current_fcb_fn
LD B, CCP_COMMAND_SIZE
.cmp_nxt:
LD A, (DE)
CP (HL) ; -> 'DIR'
JP NZ, .no_eq
INC DE
INC HL
DEC B
JP NZ, .cmp_nxt
LD A, (DE)
CP ASCII_SP
JP NZ, .inc_next
LD A, C
RET
.no_eq:
INC HL
DEC B
JP NZ, .no_eq
.inc_next:
INC C
JP .cmd_next
; --------------------------------------------------
; Command handlers ref table
; --------------------------------------------------
ccp_commands:
DW ccp_dir
DW ccp_read
DW ccp_write
DW ccp_ret ; r8
; DW ccp_exit1 ; r8
ccp_ret:
LD HL, (CPM_VARS.saved_stack_ptr)
LD SP, HL
JP CCP_RAM.ccp_unk_cmd
;ccp_exit1:
; JP MON.mon_hexb
; --------------------------------------------------
; DIR [filemask] command handler
; --------------------------------------------------
ccp_dir:
CALL CCP_RAM.ccp_cv_first_to_fcb
CALL CCP_RAM.ccp_drive_sel
; chech some filemask specified in command line
LD HL, CCP_RAM.ccp_current_fcb_fn
LD A, (HL)
CP ' '
JP NZ, .has_par
; no filemask, fill with wildcard '?'
LD B, 11
.fill_wildcard:
LD (HL), '?'
INC HL
DEC B
JP NZ, .fill_wildcard
; find file by specified mask
.has_par:
CALL CCP_RAM.ccp_find_first
JP NZ, .f_found
; no files found, print and exit
CALL CCP_RAM.ccp_out_no_file
JP CCP_RAM.ccp_cmdline_back
.f_found:
CALL CCP_RAM.ccp_out_crlf
LD HL, 0x0
LD (CPM_VARS.tmp_dir_total), HL
LD E, 0
.do_next_direntry:
PUSH DE
CALL CCP_RAM.ccp_find_first
POP DE
PUSH DE
; Find file with e number
.find_file_e:
DEC E
JP M, .file_out_next
PUSH DE
CALL CCP_RAM.ccp_bdos_find_next
POP DE
JP Z, .file_e_found
JP .find_file_e
.file_out_next:
LD A, (CCP_RAM.ccp_bdos_result_code)
; calc address of DIR entry in DMA buffer
; A[6:5] = A[1:0] = 32*A
RRCA ; [C] -> [7:0] -> [C]
RRCA ;
RRCA ;
AND 01100000b ; mask
LD C, A
PUSH BC
CALL CCP_RAM.ccp_out_crlf ; start new line
CALL CCP_RAM.ccp_bdos_drv_get
INC A
LD (dma_buffer), A ; disk
POP BC
LD B, 0x0
LD HL, dma_buffer+FCB_FN ; filename
LD E, L
LD D, H
ADD HL, BC
; copy filename to tmp FCB and out to screen
LD B, 0x1
.copy_next:
LD A, (HL)
LD (DE), A
LD C, A
CALL BIOS.conout_f
INC HL
INC DE
INC B
LD A, B
CP FN_LEN ; >12 end of name
JP C, .copy_next
.zero_up_36:
XOR A
LD (DE), A ; zero at end
INC B
LD A, B
CP 36
JP C, .zero_up_36
; calc file size for current entry
LD DE, dma_buffer
CALL cpp_bdos_f_size
LD HL, (fcb_ra_record_num) ; file size in blocks
; get disk blk size
LD A, (CPM_VARS.bdos_curdsk)
OR A
JP NZ, .no_dsk_a0
LD B, 3 ; for A - blk=3
JP .dsk_a0
.no_dsk_a0:
LD B, 4 ; for other disks - blk=4
.dsk_a0:
LD C, L
; convert 128b OS block to disk blocks
.mul_to_dsk_blk:
XOR A
LD A, H
RRA
LD H, A
LD A, L
RRA
LD L, A
DEC B
JP NZ, .mul_to_dsk_blk
; round up
LD A, (CPM_VARS.bdos_curdsk)
OR A
JP NZ, .no_dsk_a1
LD A, 00000111b ; for A - ~(~0 << 3)
JP .ds_skip1
.no_dsk_a1:
LD A, 00001111b ; for other dsk - ~(~0 << 4)
.ds_skip1:
AND C
JP Z, .cvt_blk_kb
INC HL
; Convert blocks to kilobytes (A-1k B-2k)
.cvt_blk_kb:
LD A, (CPM_VARS.bdos_curdsk)
OR A
JP Z, .ds_skip2
ADD HL, HL ; 2k
; add file size to total dir size
.ds_skip2:
EX DE, HL
LD HL, (CPM_VARS.tmp_dir_total)
ADD HL, DE
LD (CPM_VARS.tmp_dir_total), HL
; display size in K
LD C, ' '
CALL BIOS.conout_f
CALL BIOS.conout_f
CALL ccp_cout_num
LD C, 'K'
CALL BIOS.conout_f
CALL CCP_RAM.ccp_getkey_no_wait
JP NZ, CCP_RAM.ccp_cmdline_back
POP DE
INC E
JP .do_next_direntry
.file_e_found:
POP DE
LD HL, msg_free_space ;= "\r\nFREE SPACE "
; Out: FREE SPACE
CALL ccp_out_str_z
LD A, (CPM_VARS.bdos_curdsk)
OR A
JP NZ, .no_ram_dsk
LD HL, (BIOS.disk_a_size)
JP .calc_remanis_ds
.no_ram_dsk:
DEC A
LD HL, (BIOS.disk_b_size)
JP Z, .calc_remanis_ds
LD HL, (BIOS.disk_c_size)
LD A, (disk_sw_trk)
OR A
JP NZ, .calc_remanis_ds
LD A, H
CP 1
JP Z, .d720
LD HL, 360
JP .calc_remanis_ds
.d720:
LD HL, 720
; Disk size - Dir size = Free
.calc_remanis_ds:
EX DE, HL
LD HL, (CPM_VARS.tmp_dir_total)
LD A, E
SUB L
LD E, A
LD A, D
SBC A, H
LD D, A
CALL ccp_cout_num
LD C, 'K'
CALL BIOS.conout_f
CALL CCP_RAM.ccp_out_crlf
JP CCP_RAM.ccp_cmdline_back
msg_free_space:
DB "\r\nFREE SPACE ",0
ccp_cout_num:
LD A, D
AND 11100000b
JP Z, .less_224
LD C, '*'
CALL BIOS.conout_f
CALL BIOS.conout_f
CALL BIOS.conout_f
CALL BIOS.conout_f
RET
.less_224:
LD HL, 0x0
; copy number to BC
LD B, D
LD C, E
LD DE, 0x1
LD A, 13
.bc_rra:
PUSH AF
PUSH HL
; BC >> 1
LD A, B
RRA
LD B, A
LD A, C
RRA
LD C, A
JP NC, .bc_rra_ex
POP HL
CALL cpp_daa16
PUSH HL
.bc_rra_ex:
LD L, E
LD H, D
CALL cpp_daa16
EX DE, HL
POP HL
POP AF
DEC A
JP NZ, .bc_rra
LD D, 4
LD B, 0
.next_d:
LD E, 4
.next_e:
LD A, L
RLA
LD L, A
LD A, H
RLA
LD H, A
LD A, C
RLA
LD C, A
DEC E
JP NZ, .next_e
LD A, C
AND 0xf
ADD A, '0'
CP '0'
JP NZ, .no_zero
DEC B
INC B
JP NZ, .b_no_one
LD A, D
DEC A
JP Z, .d_one
LD A, ' '
JP .b_no_one
.d_one:
LD A, '0'
.no_zero:
LD B, 0x1
.b_no_one:
LD C, A
CALL BIOS.conout_f
DEC D
JP NZ, .next_d
RET
; -------------------------------------------------------
; ADD with correction HL=HL+DE
; -------------------------------------------------------
cpp_daa16:
LD A, L
ADD A, E
DAA
LD L, A
LD A, H
ADC A, D
DAA
LD H, A
RET
; -------------------------------------------------------
; Call BDOS function 35 (F_SIZE) - Compute file size
; -------------------------------------------------------
cpp_bdos_f_size:
LD C, F_SIZE
JP jp_bdos_enter
; -------------------------------------------------------
; Read Intel HEX data from serial port
; -------------------------------------------------------
ccp_read:
LD DE, msg_read_hex
CALL out_dollar_str
LD HL, 0x0
LD (CCP_RAM.hex_length), HL
; Wait for start of Intel HEX line
.wait_colon:
CALL MON.mon_serial_in
CP ':'
JP NZ, .wait_colon
; Init checksum
XOR A
LD D, A
CALL ser_read_hexb ; read byte_count
JP Z, .end_of_file
LD E, A
CALL ser_read_hexb ; read address hi
LD H, A
CALL ser_read_hexb ; read address lo
LD L, A ; HL - dst address
CALL ser_read_hexb ; read rec type
; calculate length += byte_count
PUSH HL
LD HL, (CCP_RAM.hex_length)
LD A, L
ADD A, E
LD L, A
LD A, H
ADC A, 0
LD H, A
LD (CCP_RAM.hex_length), HL
POP HL
LD C, E
; receive next E=byte_count bytes
.receive_rec:
CALL ser_read_hexb
LD (HL), A
INC HL
DEC E
JP NZ, .receive_rec
CALL ser_read_hexb ; receive checksum
JP NZ, .load_error ; jump if error
JP .wait_colon ; jump to wait next line
.end_of_file:
; read tail 4 bytes: 00 00 01 ff
CALL ser_read_hexb
CALL ser_read_hexb
CALL ser_read_hexb
CALL ser_read_hexb
JP Z, .load_complete
.load_error:
LD DE, .msg_error
JP out_dollar_str
.load_complete:
; Out message with length of received file
LD HL, (CCP_RAM.hex_length)
LD A, H
CALL MON.mon_hexb
LD A, L
CALL MON.mon_hexb
LD DE, .msg_bytes
CALL out_dollar_str
; Calculate number of pages
LD HL, (CCP_RAM.hex_length)
LD A, L
ADD A, 0xff
LD A, H
ADC A, 0x0
RLA
LD (CCP_RAM.hex_sectors), A
; Out message with number of pages
CALL MON.mon_hexb
LD DE, .msg_pages
CALL out_dollar_str
; Check for file name specified in cmd line
CALL CCP_RAM.ccp_cv_first_to_fcb
CALL CCP_RAM.ccp_drive_sel
LD A, (CCP_RAM.ccp_current_fcb_fn)
CP ASCII_SP
JP Z, .warm_boot
; Create file
LD DE, CCP_RAM.ccp_current_fcb
LD C, F_MAKE
CALL jp_bdos_enter
INC A
JP Z, .load_error
LD HL, tpa_start
LD (CCP_RAM.hex_buff), HL
.wr_sector:
; set source buffer address
LD HL, (CCP_RAM.hex_buff)
EX DE, HL
LD C, F_DMAOFF
CALL jp_bdos_enter
; write source buffer to disk
LD DE, CCP_RAM.ccp_current_fcb
LD C, F_WRITE
CALL jp_bdos_enter
; check errors
OR A
JP NZ, .load_error
; rewind forward to next sector
LD HL, (CCP_RAM.hex_buff)
LD DE, 128 ; sector size
ADD HL, DE
LD (CCP_RAM.hex_buff), HL
; decrement sector count
LD A, (CCP_RAM.hex_sectors)
DEC A
LD (CCP_RAM.hex_sectors), A
JP NZ, .wr_sector ; jump if remains sectors
; close file
LD DE, CCP_RAM.ccp_current_fcb
LD C, F_CLOSE
CALL jp_bdos_enter
; check errors
CP 0xff
JP Z, .load_error
.warm_boot:
LD C, P_TERMCPM
JP jp_bdos_enter
.msg_bytes:
DB "h bytes ($"
.msg_pages:
DB " pages)\n\r$"
.msg_error:
DB "error!\n\r$"
; ---------------------------------------------------
; Read next two symbols from serial and convert to
; byte
; Out: A - byte
; CF set if error
; ---------------------------------------------------
ser_read_hexb:
PUSH BC
CALL MON.mon_serial_in
CALL hex_to_nibble
RLCA
RLCA
RLCA
RLCA
LD C, A
CALL MON.mon_serial_in
CALL hex_to_nibble
OR C
LD C, A
ADD A, D
LD D, A
LD A, C
POP BC
RET
; ---------------------------------------------------
; Convert hex symbol to byte
; Inp: A - '0'..'F'
; Out: A - 0..15
; CF set if error
; ---------------------------------------------------
hex_to_nibble:
SUB '0' ; < '0' - error
RET C
ADD A, 233 ; F -> 255
RET C ; > F - error
ADD A, 6
JP P, .l1
ADD A, 7
RET C
.l1:
ADD A, 10
OR A
RET
; ---------------------------------------------------
; Out $ ended string
; Inp: DE -> string$
; ---------------------------------------------------
out_dollar_str:
LD C, C_WRITESTR
JP jp_bdos_enter
msg_read_hex:
DB "\n\rRead HEX from RS232... $"
filler1:
DS 62, 0
; -------------------------------------------------------
; Out zerro ended string
; In: HL -> strZ
; -------------------------------------------------------
ccp_out_str_z:
LD A, (HL)
OR A
RET Z
LD C, A
CALL BIOS.conout_f
INC HL
JP ccp_out_str_z
; -------------------------------------------------------
; Delete file and out No Space message
; -------------------------------------------------------
ccp_del_f_no_space:
LD DE, CCP_RAM.ccp_current_fcb
CALL CCP_RAM.ccp_bdos_era_file
JP CCP_RAM.ccp_no_space
; -------------------------------------------------------
; Read current file next block
; Out: A=0 - Ok, 0xFF - HW Error;
; -------------------------------------------------------
cpp_read_f_blk:
LD DE, CPM_VARS.ccp_fcb ; FCB here
JP CCP_RAM.ccp_bdos_read_f
ccp_write:
CALL CCP_RAM.ccp_cv_first_to_fcb
CALL CCP_RAM.ccp_drive_sel
LD HL, CCP_RAM.ccp_current_fcb_fn
LD A, (HL)
CP ' '
JP NZ, .find_f
LD B, 11
.fill_with_wc:
LD (HL), '?'
INC HL
DEC B
JP NZ, .fill_with_wc
.find_f:
CALL CCP_RAM.ccp_find_first
JP NZ, .found_f
CALL CCP_RAM.ccp_out_no_file
JP CCP_RAM.ccp_cmdline_back
.found_f:
LD E, 0 ; file counter
.do_next_f1:
PUSH DE
CALL CCP_RAM.ccp_find_first
POP DE
PUSH DE
.do_next_f2:
DEC E
JP M, .do_file
PUSH DE
CALL CCP_RAM.ccp_bdos_find_next
POP DE
JP Z, .no_more_f
JP .do_next_f2
.do_file:
POP BC
PUSH BC
LD A, C
OR A
JP Z, .calc_addr
LD DE, 1200
; Delay with key interrupt check
.delay_1:
XOR A
.delay_2:
DEC A
JP NZ, .delay_2
PUSH DE
CALL CCP_RAM.ccp_getkey_no_wait
POP DE
JP NZ, CCP_RAM.ccp_cmdline_back
DEC DE
LD A, D
OR E
JP NZ, .delay_1
.calc_addr:
LD A, (CCP_RAM.ccp_bdos_result_code)
; A=0-3 - for Ok, calc address of DIR entry in DMA buffer
RRCA
RRCA
RRCA
AND 01100000b
LD C, A
PUSH BC
CALL CCP_RAM.ccp_out_crlf
CALL CCP_RAM.ccp_bdos_drv_get
INC A
LD (CPM_VARS.ccp_fcb_dr), A ; set drive number
POP BC
LD B, 0x0
LD HL, dma_buffer+1
ADD HL, BC
LD DE, CPM_VARS.ccp_fcb_fn
LD B, 0x1
.copy_fn:
LD A, (HL)
LD (DE), A
INC HL
INC DE
INC B
LD A, B
CP 12
JP C, .copy_fn
.fillz_fn:
XOR A
LD (DE), A
INC DE
INC B
LD A, B
CP 36
JP C, .fillz_fn
LD HL, CPM_VARS.ccp_fcb_fn
CALL ccp_out_str_z
LD HL, dma_buffer
; Empty first 128 bytes of DMA buffer
LD B, 128
.clear_buf:
LD (HL), 0x0
INC HL
DEC B
JP NZ, .clear_buf
; Copy file name at buffer start
LD HL, dma_buffer
LD DE, CPM_VARS.ccp_fcb_fn
LD B, 8
.find_sp:
LD A, (DE)
CP ' '
JP Z, .sp_rep_dot ; ' ' -> '.'
LD (HL), A
INC HL
INC DE
JP .find_sp
.sp_rep_dot:
LD (HL), '.'
INC HL
CALL CCP_RAM.ccp_find_no_space
.cont_copy_fn:
LD A, (DE)
LD (HL), A
OR A
JP Z, .end_copy
INC HL
INC DE
JP .cont_copy_fn
.end_copy:
LD DE, CPM_VARS.ccp_fcb
CALL CCP_RAM.ccp_bdos_open_f
LD DE, 0x8000 ; Block ID
LD HL, dma_buffer
CALL cpp_pause_tape_wr_blk
CALL cpp_pause_tape_wr_blk
LD DE, 0x1
; Read file block and write to Tape
.read_f_write_t:
PUSH DE
CALL cpp_read_f_blk
; a=0xff if error; a=1 - EOF
DEC A
JP Z, .eof
LD A, (CPM_VARS.ccp_fcb_cr)
AND 0x7f
CP 0x1
JP NZ, .write_once
; Write block to Tape with ID=0 twice
LD DE, 0x0 ; Block ID=0
LD HL, dma_buffer
CALL cpp_pause_tape_wr_blk
.write_once:
CALL CCP_RAM.ccp_getkey_no_wait
LD HL, dma_buffer
POP DE
JP NZ, CCP_RAM.ccp_cmdline_back
CALL cpp_pause_tape_wr_blk
; Inc Block ID and continue
INC DE
JP .read_f_write_t
.eof:
POP DE
EX DE, HL
LD (dma_buffer), HL
EX DE, HL
; Final block ID=0xFFFF
LD DE, 0xffff
; Write twice
CALL cpp_pause_tape_wr_blk
CALL cpp_pause_tape_wr_blk
POP DE
INC E
JP .do_next_f1
.no_more_f:
POP DE
CALL CCP_RAM.ccp_out_crlf
JP CCP_RAM.ccp_cmdline_back
; -------------------------------------------------------
; Write block to tape after pause
; -------------------------------------------------------
cpp_pause_tape_wr_blk:
LD BC, 3036
.delay:
DEC BC
LD A, B
OR C
JP NZ, .delay
JP BIOS.tape_write_f
DB 0x4c
; -------------------------------------------------------
; Filler to align blocks in ROM
; -------------------------------------------------------
LAST EQU $
CODE_SIZE EQU LAST-0xDB00
FILL_SIZE EQU 0x500-CODE_SIZE
DISPLAY "| CCP_ROM\t| ",/H,ccp_entry," | ",/H,CODE_SIZE," | ",/H,FILL_SIZE," |"
; Check integrity
ASSERT ccp_dir = 0xdb62
ASSERT ccp_dir.find_file_e = 0xdb97
ASSERT ccp_dir.file_out_next = 0xdba6
ASSERT ccp_dir.file_e_found = 0xdc3e
ASSERT ccp_dir.calc_remanis_ds = 0xdc72
ASSERT ccp_dir.no_ram_dsk = 0xdc52
ASSERT msg_free_space = 0xdc8a
ASSERT ccp_cout_num = 0xdc9a
FILLER
DS FILL_SIZE-1, 0xff
DB 0xaa
ENDMODULE
IFNDEF BUILD_ROM
OUTEND
ENDIF

View File

@ -1,46 +0,0 @@
; ======================================================
; Ocean-240.2
; CP/M Combine file. Includes all sources to build
; ROM 0xC000
;
; Disassembled by Romych 2025-02-12
; ======================================================
DEFINE BUILD_ROM
DEVICE NOSLOT64K
;
; |-----------|---------------|-----------|--------------------------------------|
; | OFFSET | SIZE | Module | Memory Address |
; |-----------|---------------|-----------|--------------------------------------|
; | 0x0000 | 2048 (0x800) | CCP_RAM | 0xC000..0xC7FF -> RAM 0xB200..0xB5FF |
; | 0x0800 | 3584 (0xE00) | BDOS | 0xC800.. |
; | 0x1600 | 1024 (0x400) | BIOS | 0xD600..D9FF |
; | 0x1B00 | 1280 (0x500) | CCP_ROM | 0xDB00..DFFF |
; |-----------|---------------|-----------|--------------------------------------|
;
DISPLAY "| Module | Offset | Size | Free |"
DISPLAY "|-------------|---------|--------|--------|"
OUTPUT cpm-C000.bin
;LABELSLIST "cpm.labels"
;CSPECTMAP "cpm.map"
INCLUDE "equates.inc"
INCLUDE "bdos.inc"
INCLUDE "ccp_ram.asm"
INCLUDE "bdos.asm"
INCLUDE "bios.asm"
INCLUDE "ccp_rom.asm"
OUTEND
OUTPUT variables.bin
INCLUDE "cpm_vars.inc"
OUTEND
END

File diff suppressed because it is too large Load Diff

View File

@ -1,43 +0,0 @@
; ==================================================
; Simple CP/M program
; By Romych
; ==================================================
DEVICE NOSLOT64K
SLDOPT COMMENT WPMEM, ASSERTION, LOGPOINT
; INCLUDE "ok240/equates.inc"
INCLUDE "ok240/bdos.inc"
OUTPUT main.com
ORG 0x100
LD B, 10
LD SP, stack
AGAIN:
LD DE, message
; ASSERTION B < 11
LD A, 10
SUB B
OR 0x30
LD (DE), A
LD C, C_WRITESTR
PUSH BC
CALL BDOS_ENTER
POP BC
DEC B ; LOGPOINT
JP NZ, AGAIN
JP WARM_BOOT
message: ; WPMEM, 1, w
DB "n - Welcome to OK240.2!\r\n$"
OUTEND
;DS 1024
stack EQU 0xbfc0
;DISPLAY "message: EQU\t| ",/H,message
END

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +0,0 @@
; =======================================================
; Ocean-240.2
; CP/M v2.2. Equates to call BDOS functions
;
; Created by Romych 2026-02-16
; =======================================================
IFNDEF _BDOS_INC
DEFINE _BDOS_INC
; ------------------------------------------------------
; CP/M Entries and variables
; ------------------------------------------------------
WARM_BOOT EQU 0x0000 ; Jump warm_boot (Restart)
IO_BYTE EQU 0x0003 ; Input/Output mapping
CUR_USER_DRV EQU 0x0004 ; [7:4] - curent user, [3:0] - current drive
BDOS_ENTER EQU 0x0005 ; Jump bdos (CALL 5 to make CP/M requests)
FCB1 EQU 0x005c ; Default FCB, 16 bytes
FCB2 EQU 0x006c
DMA_BUFFER EQU 0x0080 ; Default "DMA" 128 bytes buffer
CMD_LINE_LEN EQU 0x0080 ; command line character count
CMD_LINE EQU 0x0081 ; command line buffer
TPA EQU 0x0100 ; start of program
; ------------------------------------------------------
; CP/M BDOS Functions
; ------------------------------------------------------
P_TERMCPM EQU 0 ; System Reset
C_READ EQU 1 ; Console input
C_WRITE EQU 2 ; Console output
A_READ EQU 3 ; Auxiliary (Reader) input
A_WRITE EQU 4 ; Auxiliary (Punch) output
L_WRITE EQU 5 ; Printer output
C_RAWIO EQU 6 ; Direct console I/O
A_STATIN EQU 7 ; Auxiliary Input status
A_STATOUT EQU 8 ; Auxiliary Output status
C_WRITESTR EQU 9 ; Output string
C_READSTR EQU 10 ; Buffered console input
C_STAT EQU 11 ; Console status
S_BDOSVER EQU 12 ; Return version number
DRV_ALLRESET EQU 13 ; Reset discs
DRV_SET EQU 14 ; Select disc
F_OPEN EQU 15 ; Open file
F_CLOSE EQU 16 ; Close file
F_SFIRST EQU 17 ; search for first
F_SNEXT EQU 18 ; search for next
F_DELETE EQU 19 ; delete file
F_READ EQU 20 ; read next record
F_WRITE EQU 21 ; write next record
F_MAKE EQU 22 ; create file
F_RENAME EQU 23 ; Rename file
DRV_LOGINVEC EQU 24 ; Return bitmap of logged-in drives
DRV_GET EQU 25 ; Return current drive
F_DMAOFF EQU 26 ; Set DMA address
DRV_ALLOCVEC EQU 27 ; Return address of allocation map
DRV_SETRO EQU 28 ; Software write-protect current disc
DRV_ROVEC EQU 29 ; Return bitmap of read-only drives
F_ATTRIB EQU 30 ; set file attributes
DRV_DPB EQU 31 ; get DPB address
F_USERNUM EQU 32 ; get/set user number
F_READRAND EQU 33 ; Random access read record
F_WRITERAND EQU 34 ; Random access write record
F_SIZE EQU 35 ; Compute file size
F_RANDREC EQU 36 ; Update random access pointer
DRV_RESET EQU 37 ; Selectively reset disc drives
DRV_ACCESS EQU 38 ; Access drives
DRV_FREE EQU 39 ; Free drive
F_WRITEZF EQU 40 ; Write random with zero fill
ENDIF

View File

@ -1,104 +0,0 @@
; ======================================================
; Ocean-240.2
; Equates for all assembly sources
;
; By Romych 2026-03-10
; ======================================================
IFNDEF _EQUATES
DEFINE _EQUATES
ASCII_BELL EQU 0x07 ; Make Beep
ASCII_BS EQU 0x08 ; Move cursor left (Back Space)
ASCII_TAB EQU 0x09 ; Move cursor right +8 pos
ASCII_LF EQU 0x0A ; Move cursor down (Line Feed)
ASCII_FF EQU 0x0C ; Move cursor to home (Form Feed)
ASCII_CR EQU 0x0D ; Move gursor to 1st pos (Cariage Return)
ASCII_CAN EQU 0x18 ; Move cursor right
ASCII_EM EQU 0x19 ; Move cursor up
ASCII_SUB EQU 0x1A ; CTRL-Z - end of text file marker
ASCII_ESC EQU 0x1B ;
ASCII_US EQU 0x1F ; Clear screen
ASCII_SP EQU 0x20
ASCII_DEL EQU 0x7F
; ------------------------------------------------------
BELL_PIN EQU 0x08 ; DD67 Pin PC3 - "BELL"
; ------------------------------------------------------
CTRL_A EQU 0x01
CTRL_B EQU 0x02
CTRL_C EQU 0x03 ; Warm boot
CTRL_D EQU 0x04
CTRL_E EQU 0x05 ; Move to beginning of new line (Physical EOL)
CTRL_F EQU 0x06
CTRL_G EQU 0x07
CTRL_H EQU 0x08 ; Backspace
CTRL_I EQU 0x09
CTRL_J EQU 0x0A ; LF - Line Feed
CTRL_K EQU 0x0B
CTRL_L EQU 0x0C
CTRL_M EQU 0x0D ; CR - Carriage Return
CTRL_N EQU 0x0E
CTRL_O EQU 0x0F
CTRL_P EQU 0x10 ; turn on/off printer
CTRL_Q EQU 0x11
CTRL_R EQU 0x12 ; Repeat current cmd line
CTRL_S EQU 0x13 ; Temporary stop display data to console (aka DC3)
CTRL_T EQU 0x14
CTRL_U EQU 0x15 ; Cancel (erase) current cmd line
CTRL_V EQU 0x16
CTRL_W EQU 0x17
CTRL_X EQU 0x18 ; Cancel (erase) current cmd line
CTRL_Y EQU 0x19
CTRL_Z EQU 0x1A ; CTRL-Z - end of text file marker
; ------------------------------------------------------
TRUE EQU 0xFF
FALSE EQU 0x00
; ---------------------------------------------------
; FCB Offsets
; ---------------------------------------------------
FCB_LEN EQU 32 ; length of FCB
FCB_SHF EQU 5
FN_LEN EQU 12 ; Length of filename in FCB
FCB_DR EQU 0 ; Drive. 0 for default, 1-16 for A-P
FCB_FN EQU 1 ; Fn - Filename, 7-bit ASCII. The top bits - attributes
FCB_FT EQU 9 ; Filetype, 7-bit ASCII.
; T1' to T3' have the following
; T1' - Read-Only
; T2' - System (hidden)
; T3' - Archive
FCB_EXT EQU 12 ; EX - Set this to 0 when opening a file and then leave it to
; CP/M. You can rewind a file by setting EX, RC, S2 and CR to 0.
FCB_S1 EQU 13 ; S1 - Reserved.
FCB_S2 EQU 14 ; S2 - Reserved. Bit 7 - wile write flag, [6:0] module number (Extent hi bits)
FCB_RC EQU 15 ; RC - Set this to 0 when opening a file and then leave it to CP/M.
FCB_AL EQU 16 ; AL - Image of the second half of the directory entry,
; containing the file's allocation (which disc blocks it owns).
FCB_CR EQU 32 ; CR - Current record within extent. It is usually best to set
; this to 0 immediately after a file has been opened and
; then ignore it.
FCB_RN EQU 33 ; Rn - Random access record number. A 16-bit (with R2 used for overflow)
; ------------------------------------------------------
IRQ_0 EQU 0x01
IRQ_1 EQU 0x02
IRQ_2 EQU 0x04
IRQ_KEYBOARD EQU 0x02
IRQ_PRINTER EQU 0x08
IRQ_TIMER EQU 0x10
KBD_ACK EQU 0x10
PRINTER_ACK EQU 0x10
TX_READY EQU 0x01
RX_READY EQU 0x02
TMR0_SQWAVE EQU 0x36
; ------------------------------------------------------
ENDIF

View File

@ -1,132 +0,0 @@
; =======================================================
; Ocean-240.2
; Computer with FDC variant.
; IO Ports definitions
;
; By Romych 2025-09-09
; =======================================================
IFNDEF _IO_PORTS
DEFINE _IO_PORTS
; -------------------------------------------------------
; КР580ВВ55 DD79
; -------------------------------------------------------
; Port A - User port A
USR_DD79PA EQU 0x00
; Port B - User port B
USR_DD79PB EQU 0x01
; Port C - User port C
USR_DD79PC EQU 0x02
; Config: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
; Set bit: [0][xxx][bbb][0|1]
USR_DD79CTR EQU 0x03
; -------------------------------------------------------
; КР1818ВГ93
; -------------------------------------------------------
; CMD
FDC_CMD EQU 0x20
; TRACK
FDC_TRACK EQU 0x21
; SECTOR
FDC_SECT EQU 0x22
; DATA
FDC_DATA EQU 0x23
;
FDC_DRQ EQU 0x24
; Controller port
FLOPPY EQU 0x25
; -------------------------------------------------------
; КР580ВВ55 DD78
; -------------------------------------------------------
; Port A - Keyboard Data
KBD_DD78PA EQU 0x40
; Port B - JST3,SHFT,CTRL,ACK,TAPE5,TAPE4,GK,GC
KBD_DD78PB EQU 0x41
; Port C - [PC7..0]
KBD_DD78PC EQU 0x42
; Сonfig: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
; Set bit: [0][xxx][bbb][0|1];
KBD_DD78CTR EQU 0x43
; -------------------------------------------------------
; КР580ВИ53 DD70
; -------------------------------------------------------
; Counter 1
TMR_DD70C1 EQU 0x60
; Counter 2
TMR_DD70C2 EQU 0x61
; Counter 3
TMR_DD70C3 EQU 0x62
; Config: [sc1,sc0][rl1,rl0][m2,m1,m0][bcd]
; sc - timer, rl=01-LSB, 10-MSB, 11-LSB+MSB
; mode 000 - int on fin,
; 001 - one shot,
; x10 - rate gen,
; x11-sq wave
TMR_DD70CTR EQU 0x63
; Programable Interrupt controller PIC KR580VV59
PIC_DD75A EQU 0x80
PIC_DD75B EQU 0x81
; -------------------------------------------------------
; КР580ВВ51 DD72
; -------------------------------------------------------
; Data
UART_DD72RD EQU 0xA0
; [RST,RQ_RX,RST_ERR,PAUSE,RX_EN,RX_RDY,TX_RDY]
UART_DD72RR EQU 0xA1
; -------------------------------------------------------
; КР580ВВ55 DD17
; -------------------------------------------------------
; Port A - VShift[8..1]
SYS_DD17PA EQU 0xC0
; Port B - [ROM14,13][REST][ENROM-][A18,17,16][32k]
SYS_DD17PB EQU 0xC1
; Port C - HShift[HS5..1,SB3..1]
SYS_DD17PC EQU 0xC2
; Сonfig: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
; Set bit: [0][xxx][bbb][0|1];
SYS_DD17CTR EQU 0xC3
; -------------------------------------------------------
; КР580ВВ55 DD67
; -------------------------------------------------------
; Port A - LPT Data
LPT_DD67PA EQU 0xE0
; Port B - [VSU,C/M,FL3..1,COL3..1]
VID_DD67PB EQU 0xE1
; Port C - [USER3..1,STB-LP,BELL,TAPE3..1]
DD67PC EQU 0xE2
; Сonfig: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
; Set bit: [0][xxx][bbb][0|1];
DD67CTR EQU 0xE3
ENDIF

View File

@ -1,31 +0,0 @@
; =======================================================
; Ocean-240.2
; Equates to call Monitor v8 functions
;
; Created by Romych 2026-02-16
; =======================================================
IFNDEF _MON_INC
DEFINE _MON_INC
mon_start EQU 0xE000
mon_hexb EQU 0xE003
non_con_status EQU 0xE006
mon_con_in EQU 0xE009
mon_con_out EQU 0xE00C
mon_serial_in EQU 0xE00F
mon_serial_out EQU 0xE012
mon_char_print EQU 0xE015
mon_tape_read EQU 0xE018
mon_tape_write EQU 0xE01B
mon_ram_disk_read EQU 0xE01E
mon_ram_disk_write EQU 0xE021
mon_tape_read_ram EQU 0xE024
mon_tape_write_ram EQU 0xE027
mon_tape_wait EQU 0xE02A
mon_tape_detect EQU 0xE02D
mon_read_floppy EQU 0xE030
mon_write_floppy EQU 0xE033
mon_out_str_z EQU 0xE036
ENDIF

View File

@ -1,41 +0,0 @@
; =======================================================
; Ocean-240.2
;
; RAM area at address: 0x0000 - 0x0100, used by CP/M and
; HW-Monitor
; By Romych 2026-02-03
; =======================================================
IFNDEF _RAM
DEFINE _RAM
MODULE RAM
@warm_boot EQU 0x0000 ; Jump warm_boot (Restart)
@warm_boot_addr EQU 0x0001 ; address of warm boot entry point
@iobyte EQU 0x0003 ; Input/Output mapping
@cur_user_drv EQU 0x0004 ; [7:4] - curent user, [3:0] - current drive
@bdos_enter EQU 0x0005 ; Jump bdos (CALL 5 to make CP/M requests)
@bdos_ent_addr EQU 0x0006 ; addres of BDOS entry point
;@RST1 EQU 0x0008
;@RST1_handler_addr EQU 0x0009
;RST2 EQU 0x0010
;RST3 EQU 0x0018
;RST4 EQU 0x0020
;RST5 EQU 0x0028
;RST6 EQU 0x0030
;RST7 EQU 0x0038
@fcb1 EQU 0x005c ; Default FCB, 16 bytes
@fcb2 EQU 0x006c
;NMI_ISR EQU 0x0066
@dma_buffer EQU 0x0080 ; Default "DMA" 128 bytes buffer
@p_cmd_line_len EQU 0x0080 ; command line character count
@p_cmd_line EQU 0x0081 ; command line buffer
@bios_stack EQU 0x0100
@tpa_start EQU 0x0100 ; start of program
@video_ram EQU 0x4000
ENDMODULE
ENDIF

View File

@ -1,14 +0,0 @@
# Эмулятор персонального компьютера Океан-240.2
## Образы дискет для эмулятора.
В файле floppyB.okd собрано большинство софта для компьютера. По умолчанию, при запуске эмулятора, это дискета B:.
Софт отсортирован по типам для разных юзеров.
- USER 0 - утилиты ОС CP/M
- USER 1 - Ассемблеры, линкеры
- USER 2 - MSBASIC и программы *.BAS
- USER 3 - Игры
- USER 5 - ZEXDOC, ZEXALL - тесты процессора Z80
Файл empty.okd - это образ пустой, отформатированной дискеты.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

10
go.mod Normal file
View File

@ -0,0 +1,10 @@
module z80em
go 1.25
require (
github.com/sirupsen/logrus v1.9.4
gopkg.in/yaml.v3 v3.0.1
)
require golang.org/x/sys v0.13.0 // indirect

14
go.sum Normal file
View File

@ -0,0 +1,14 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 210 KiB

View File

@ -3,7 +3,6 @@ package logger
import (
"bufio"
"fmt"
"okemu/config"
"os"
"path/filepath"
"strconv"
@ -46,12 +45,15 @@ func InitLogging() {
// FlushLogs Flush logs if logging to file
func FlushLogs() {
if logBuffer != nil {
_ = logBuffer.Flush()
err := logBuffer.Flush()
if err != nil {
return
}
}
}
// ReconfigureLogging Reconfigure logging by config values
func ReconfigureLogging(config *config.OkEmuConfig) {
func ReconfigureLogging(config *config.ServiceConfig) {
// redirect logging if log file specified
if len(config.LogFile) > 0 {
var err error

29
main.go Normal file
View File

@ -0,0 +1,29 @@
package main
import (
"z80em/config"
"z80em/logger"
"z80em/okean240"
)
var Version = "v1.0.0"
var BuildTime = "2026-03-01"
func main() {
// base log init
logger.InitLogging()
// load config yml file
config.LoadConfig()
conf := config.GetConfig()
// Reconfigure logging by config values
logger.ReconfigureLogging(conf)
println("Init computer")
computer := okean240.New(config)
println("Run computer")
computer.Run()
}

BIN
okean240/MON_r6.BIN Normal file

Binary file not shown.

75
okean240/computer.go Normal file
View File

@ -0,0 +1,75 @@
package okean240
import (
"z80em/config"
"z80em/z80em"
log "github.com/sirupsen/logrus"
)
type ComputerType struct {
cpu z80em.Z80Type
memory Memory
ioPorts [256]byte
cycles uint64
dd17EnableOut bool
}
type ComputerInterface interface {
//Init(rom0 string, rom1 string)
Run()
}
func (c *ComputerType) M1MemRead(addr uint16) byte {
return c.memory.M1MemRead(addr)
}
func (c *ComputerType) MemRead(addr uint16) byte {
return c.memory.MemRead(addr)
}
func (c *ComputerType) MemWrite(addr uint16, val byte) {
c.memory.MemWrite(addr, val)
}
func (c *ComputerType) IORead(port uint16) byte {
return c.ioPorts[port]
}
func (c *ComputerType) IOWrite(port uint16, val byte) {
c.ioPorts[byte(port&0x00ff)] = val
switch byte(port & 0x00ff) {
case SYS_DD17PB:
if c.dd17EnableOut {
c.memory.Configure(val)
}
case SYS_DD17CTR:
c.dd17EnableOut = val == 0x80
case KBD_DD78CTR:
}
}
// New Builds new computer
func New(cfg config.OkEmuConfig) *ComputerType {
c := ComputerType{}
c.memory = Memory{}
c.memory.Init(cfg.MonitorFile, cfg.CPMFile)
c.cpu = *z80em.New(&c)
c.cycles = 0
c.dd17EnableOut = false
return &c
}
func (c *ComputerType) Run() {
c.cpu.Reset()
for {
state := c.cpu.GetState()
log.Infof("%d - [%x]: %x\n", c.cycles, state.PC, c.MemRead(state.PC))
c.cycles += uint64(c.cpu.RunInstruction())
}
}

154
okean240/ioports.go Normal file
View File

@ -0,0 +1,154 @@
package okean240
/*
* Ocean-240.2
* Computer with FDC variant.
* IO Ports definitions
*
* By Romych 2026-03-01
*/
/*
* КР580ВВ55 DD79 USER PORT
*/
// USR_DD79PA User port A
const USR_DD79PA = 0x00
// USR_DD79PB User port B
const USR_DD79PB = 0x01
// USR_DD79PC User port C
const USR_DD79PC = 0x02
// USR_DD79CTR Config
const USR_DD79CTR = 0x03 // Config: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
// Set bit: [0][xxx][bbb][0|1]
/*
* КР1818ВГ93 FDC Controller
*/
// FDC_CMD FDC Command
const FDC_CMD = 0x20
// FDC_TRACK FDC Track No
const FDC_TRACK = 0x21
// FDC_SECT FDC Sector
const FDC_SECT = 0x22
// FDC_DATA FDC Data
const FDC_DATA = 0x23
// FDC_WAIT FDC Wait
const FDC_WAIT = 0x24
/*
* Floppy Controller port
*/
// FLOPPY Floppy Controller port
const FLOPPY = 0x25 // WR: 5-SSEN, 4-#DDEN, 3-INIT, 2-DRSEL, 1-MOT1, 0-MOT0
// RD: 7-MOTST, 6-SSEL, 5,4-x , 3-DRSEL, 2-MOT1, 1-MOT0, 0-INT
/*
* КР580ВВ55 DD78 Keyboard
*/
// KBD_DD78PA Port A - Keyboard Data
const KBD_DD78PA = 0x40
// KBD_DD78PB Port B - JST3,SHFT,CTRL,ACK,TAPE5,TAPE4,GK,GC
const KBD_DD78PB = 0x41
// KBD_DD78PC Port C - [PC7:5],[KBD_ACK],[PC3:0]
const KBD_DD78PC = 0x42
// KBD_DD78CTR Control port
const KBD_DD78CTR = 0x43 // Сonfig: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
// Set bit: [0][xxx][bbb][0|1]
/*
* КР580ВИ53 DD70
*/
// TMR_DD70C1 Timer counter 1
const TMR_DD70C1 = 0x60
// TMR_DD70C2 Timer counter 2
const TMR_DD70C2 = 0x61
// TMR_DD70C3 Timer counter 3
const TMR_DD70C3 = 0x62
/*
TMR_DD70CTR
Timer config: [sc1,sc0][rl1,rl0][m2,m1,m0][bcd]
sc - timer, rl=01-LSB, 10-MSB, 11-LSB+MSB
mode 000 - int on fin,
001 - one shot,
x10 - rate gen,
x11-sq wave
*/
const TMR_DD70CTR = 0x63
/*
* Programmable Interrupt controller PIC KR580VV59
*/
// PIC_DD75RS RS Port
const PIC_DD75RS = 0x80
// PIC_DD75RM RM Port
const PIC_DD75RM = 0x81
/*
* КР580ВВ51 DD72
*/
// UART_DD72RD Serial data
const UART_DD72RD = 0xA0
// UART_DD72RR Serial status [RST,RQ_RX,RST_ERR,PAUSE,RX_EN,RX_RDY,TX_RDY]
const UART_DD72RR = 0xA1
/*
* КР580ВВ55 DD17 System port
*/
// Port A - VShift[8..1] Vertical shift
const SYS_DD17PA = 0xC0
// Port B - Memory mapper [ROM14,13][REST][ENROM-][A18,17,16][32k]
const SYS_DD17PB = 0xC1
// Port C - HShift[HS5..1,SB3..1] Horisontal shift
const SYS_DD17PC = 0xC2
/*
* SYS_DD17CTR
* Сonfig: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
* Set bit: [0][xxx][bbb][0|1]
*/
const SYS_DD17CTR = 0xC3
/*
* КР580ВВ55 DD67
*/
// LPT_DD67PA Port A - Printer Data
const LPT_DD67PA = 0xE0
// VID_DD67PB Port B - Video control [VSU,C/M,FL3:1,COL3:1]
const VID_DD67PB = 0xE1
// DD67PC Port C - [USER3:1, STB-LP, BELL, TAPE3:1]
const DD67PC = 0xE2
/*
* DD67CTR
* Сonfig: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
* Set bit: [0][xxx][bbb][0|1]
*/
const DD67CTR = 0xE3

View File

@ -24,21 +24,20 @@ const (
WindowNo3
)
type MemoryBlock struct {
id byte
type RamBlock struct {
memory [RamBlockSize]byte
}
type Memory struct {
allMemory [RamBlocks]*MemoryBlock
memoryWindow [RamWindows]*MemoryBlock
rom0 MemoryBlock // monitor + monitor
rom1 MemoryBlock // cpm + monitor
allMemory [RamBlocks]RamBlock
memoryWindow [RamWindows]RamBlock
rom0 RamBlock // monitor + monitor
rom1 RamBlock // cpm + monitor
config byte
}
type MemoryInterface interface {
// Init - Initialize memory at "computer started"
// Init - Initialize memory at "computer start"
Init(rom0 string, rom1 string)
// Configure - Set memory configuration
Configure(value byte)
@ -53,18 +52,15 @@ type MemoryInterface interface {
func (m *Memory) Init(monFile string, cmpFile string) {
// empty RAM
var id byte = 0
for block := range m.allMemory {
rb := MemoryBlock{}
rb.id = id
id++
rb := RamBlock{}
for addr := 0; addr < RamBlockSize; addr++ {
rb.memory[addr] = RamDefaultInitPattern
}
m.allMemory[block] = &rb
m.allMemory[block] = rb
}
// Command ROM files and init ROM0,1
// Load ROM files and init ROM0,1
// Read the entire file into a byte slice
rom0bin, err := os.ReadFile(monFile)
if err != nil {
@ -74,10 +70,8 @@ func (m *Memory) Init(monFile string, cmpFile string) {
if err != nil {
log.Fatal(err)
}
m.rom0 = MemoryBlock{}
m.rom0.id = 0xF0
m.rom1 = MemoryBlock{}
m.rom1.id = 0xF1
m.rom0 = RamBlock{}
m.rom1 = RamBlock{}
half := RamBlockSize / 2
for i := 0; i < half; i++ {
// mon+mon
@ -98,7 +92,7 @@ func (m *Memory) Configure(value byte) {
// RST bit set just after System RESET
// All memoryWindow windows points to ROM0 (monitor)
for i := 0; i < RamWindows; i++ {
m.memoryWindow[i] = &m.rom0
m.memoryWindow[i] = m.rom0
}
} else {
// Map RAM blocks to windows
@ -107,14 +101,14 @@ func (m *Memory) Configure(value byte) {
m.memoryWindow[i] = m.allMemory[sp+i]
}
// Map two hi windows to low windows in 32k flag set
if m.config&AccessHiBit == 1 {
if m.config&AccessHiBit == 0 {
m.memoryWindow[WindowNo0] = m.memoryWindow[WindowNo2]
m.memoryWindow[WindowNo1] = m.memoryWindow[WindowNo3]
}
// If ROM enabled, map ROM to last window
if m.config&ROMDisBit == 0 {
// If ROM enabled, CP/M + Mon at window 3 [0xC000:0xFFFF]
m.memoryWindow[WindowNo3] = &m.rom1
m.memoryWindow[WindowNo3] = m.rom1
}
}
}
@ -128,21 +122,5 @@ func (m *Memory) MemRead(addr uint16) byte {
}
func (m *Memory) MemWrite(addr uint16, val byte) {
window := addr >> 14
offset := addr & 0x3fff
if m.memoryWindow[window].id < 0xF0 {
// write to RAM only
m.memoryWindow[window].memory[offset] = val
} else {
log.Debugf("Attempting to write 0x%02X=>ROM[0x%04X]=", val, addr)
}
}
// MemoryWindows Return memory pages, mapped to memory windows
func (m *Memory) MemoryWindows() []byte {
var res []byte
for w := 0; w < RamWindows; w++ {
res = append(res, m.memoryWindow[w].id)
}
return res
m.memoryWindow[addr>>14].memory[addr&0x3fff] = val
}

View File

@ -1,15 +1,4 @@
logFile: okemu.log
logLevel: debug
monitorFile: rom/MON_r8_9c6c6546.mon
cpmFile: rom/CPM_r8_bc0695e4.cpm
fdc:
- autoLoad: true
autoSave: true
floppyFile: floppy/floppyB.okd
- autoLoad: false
autoSave: false
floppyFile: floppy/empty.okd
debugger:
enabled: true
host: localhost
port: 10001
logFile: "okemu.log"
logLevel: "info"
monitorFile: "okean240/MON_r6.BIN"
cpmFile: "okean240/CPM_r7.BIN"

14
rebuild.sh Executable file
View File

@ -0,0 +1,14 @@
#! /bin/bash
echo "Build OkEmu"
export GOROOT=/home/roma/projects/go/1.25.7/
export PATH=/home/roma/projects/go/1.25.7/bin/:$PATH
version=$(git describe --tags HEAD)
#commit=$(git rev-parse HEAD)
timestamp=$(date +%Y-%m-%d' '%H:%M:%S)
go build -ldflags "-X 'main.Version=$version' -X 'main.BuildTime=$timestamp'" .
#echo "Copy to kubelogtst01"
#scp stash boykovra@kubelogtst01:

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -1,3 +0,0 @@
# Эмулятор персонального компьютера Океан-240.2
Двоичные образы операционной системы и монитора, прошиваемые в ПЗУ компьютера.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,7 +0,0 @@
# Эмулятор персонального компьютера Океан-240.2#
Исходники эмулятора на языке [GO](https://go.dev/).
Для графического интерфейса используется библиотека [Fyne](https://fyne.io/). Для сборки исходников, нужно её установить по инструкции к ней.
Для вычисления выражений, используется моя модификация библиотеки [gval](github.com/PaesslerAG/gval), исходные коды в папке [gval](gval/).

View File

@ -1,131 +0,0 @@
package config
import (
"os"
log "github.com/sirupsen/logrus"
"gopkg.in/yaml.v3"
)
const defaultMonitorFile = "rom/MON_v5.bin"
const defaultCPMFile = "rom/CPM_v5.bin"
const DefaultDebufPort = 10000
const confFile = "okemu.yml"
type OkEmuConfig struct {
LogFile string `yaml:"logFile"`
LogLevel string `yaml:"logLevel"`
MonitorFile string `yaml:"monitorFile"`
CPMFile string `yaml:"cpmFile"`
FDC []FDCConfig `yaml:"fdc"`
Debugger DebuggerConfig `yaml:"debugger"`
}
type FDCConfig struct {
AutoLoad bool `yaml:"autoLoad"`
AutoSave bool `yaml:"autoSave"`
FloppyFile string `yaml:"floppyFile"`
}
type DebuggerConfig struct {
Enabled bool `yaml:"enabled"`
Host string `yaml:"host"`
Port int `yaml:"port"`
}
var config *OkEmuConfig
//func usage() {
// fmt.Printf("Use: %s --config <file_path>.yml\n", filepath.Base(os.Args[0]))
// os.Exit(2)
//}
func GetConfig() *OkEmuConfig {
return config
}
func LoadConfig() {
//args := os.Args
//if len(args) != 3 {
// usage()
//}
//
//if args[1] != "--config" {
// usage()
//}
// confFile := args[2]
conf := OkEmuConfig{}
data, err := os.ReadFile(confFile)
if err == nil {
err := yaml.Unmarshal(data, &conf)
if err != nil {
log.Panicf("Config file error: %v", err)
}
setDefaultConf(&conf)
checkConfig(&conf)
} else {
log.Panicf("Can not open config file: %v", err)
}
config = &conf
}
func checkConfig(conf *OkEmuConfig) {
if conf.Debugger.Host == "" {
conf.Debugger.Host = "localhost"
}
}
func setDefaultConf(conf *OkEmuConfig) {
if conf.LogLevel == "" {
conf.LogLevel = "info"
}
if conf.LogFile == "" {
conf.LogFile = "okemu.log"
}
if conf.MonitorFile == "" {
conf.MonitorFile = defaultMonitorFile
}
if conf.CPMFile == "" {
conf.CPMFile = defaultCPMFile
}
if conf.Debugger.Port < 80 || conf.Debugger.Port > 65535 {
log.Infof("Port %d incorrect, using default: %d", conf.Debugger.Port, DefaultDebufPort)
conf.Debugger.Port = DefaultDebufPort
}
}
func (c *OkEmuConfig) Clone() *OkEmuConfig {
fds := make([]FDCConfig, 2)
for n, fd := range c.FDC {
fds[n].FloppyFile = fd.FloppyFile
fds[n].AutoLoad = fd.AutoLoad
fds[n].AutoSave = fd.AutoSave
}
return &OkEmuConfig{
LogFile: c.LogFile,
LogLevel: c.LogLevel,
MonitorFile: c.MonitorFile,
CPMFile: c.CPMFile,
FDC: fds,
Debugger: DebuggerConfig{
Enabled: c.Debugger.Enabled,
Host: c.Debugger.Host,
Port: c.Debugger.Port,
},
}
}
func (c *OkEmuConfig) Save() {
data, err := yaml.Marshal(c)
if err != nil {
log.Errorf("config error: %v", err)
}
err = os.WriteFile(confFile, data, 0600)
if err != nil {
log.Errorf("save config file: %v", err)
}
}

View File

@ -1,369 +0,0 @@
package breakpoint
import (
"context"
"fmt"
"okemu/gval"
//"okemu/okean240"
"regexp"
"strconv"
"strings"
log "github.com/sirupsen/logrus"
)
const MaxBreakpoints = 16383
const BpTmp1 = MaxBreakpoints + 1
const BpTmp2 = MaxBreakpoints + 2
const (
BPTypeSimplePC = iota // Simple PC=nn breakpoint
BPTypeSimpleSP // Simple SP>=nn breakpoint
BPTypeExpression // Complex expression breakpoint
)
type Breakpoint struct {
addr uint16
cond string
eval gval.Evaluable
bpType int
pass uint16
passCount uint16
enabled bool
mBank uint8
}
var andMatch = regexp.MustCompile(`\s+AND\s+`)
var orMatch = regexp.MustCompile(`\s+OR\s+`)
// var xorMatch = regexp.MustCompile(`\s+XOR\s+`)
var hexHMatch = regexp.MustCompile(`[[:xdigit:]]+H`)
var eqMatch = regexp.MustCompile(`[^=><!]=[^=]`)
func patchExpression(expr string) string {
ex := strings.ToUpper(expr)
ex = andMatch.ReplaceAllString(ex, " && ")
ex = orMatch.ReplaceAllString(ex, " || ")
// ex = xorMatch.ReplaceAllString(ex, " ^ ")
for {
pos := hexHMatch.FindStringIndex(ex)
if pos != nil && len(pos) == 2 {
hex := "0x" + ex[pos[0]:pos[1]-1]
ex = ex[:pos[0]] + hex + ex[pos[1]:]
} else {
break
}
}
for {
pos := eqMatch.FindStringIndex(ex)
if pos != nil && len(pos) == 2 {
ex = ex[:pos[0]+1] + "==" + ex[pos[1]-1:]
} else {
break
}
}
ex = strings.ReplaceAll(ex, "NOT", "!")
ex = strings.ReplaceAll(ex, "<>", "!=")
return ex
}
var pcMatch = regexp.MustCompile(`^PC=[[:xdigit:]]+h$`)
var spMatch = regexp.MustCompile(`^SP>=[[:xdigit:]]+$`)
func getSecondUint16(param string, sep string) (uint16, error) {
p := strings.Split(param, sep)
v := p[1]
base := 0
if strings.HasSuffix(v, "h") || strings.HasSuffix(v, "H") {
v = strings.TrimSuffix(v, "H")
v = strings.TrimSuffix(v, "h")
base = 16
}
a, e := strconv.ParseUint(v, base, 16)
if e != nil {
return 0, e
}
return uint16(a), nil
}
func NewBreakpoint(expr string, mBank uint8) (*Breakpoint, error) {
bp := Breakpoint{
addr: 0,
enabled: false,
passCount: 0,
pass: 0,
bpType: BPTypeSimplePC,
mBank: mBank,
}
// Check if BP is simple PC=addr
expr = strings.TrimSpace(expr)
bp.cond = expr
pcMatched := pcMatch.MatchString(expr)
spMatched := spMatch.MatchString(expr)
if pcMatched {
// PC=xxxxh
bp.bpType = BPTypeSimplePC
v, e := getSecondUint16(expr, "=")
if e != nil {
return nil, e
}
bp.addr = v
} else if spMatched {
// SP>=xxxx
bp.bpType = BPTypeSimpleSP
v, e := getSecondUint16(expr, "=")
if e != nil {
return nil, e
}
bp.addr = v
} else {
// complex expression
bp.bpType = BPTypeExpression
ex := patchExpression(expr)
log.Debugf("Original Expression: '%s'", expr)
log.Debugf(" Patched Expression: '%s'", ex)
err := bp.SetExpression(ex)
if err != nil {
return nil, err
}
}
return &bp, nil
}
func (b *Breakpoint) Enabled() bool {
return b.enabled
}
func (b *Breakpoint) SetEnabled(enabled bool) {
b.enabled = enabled
}
func (b *Breakpoint) PassCount() uint16 {
return b.passCount
}
func (b *Breakpoint) SetPassCount(passCount uint16) {
b.passCount = passCount
}
func (b *Breakpoint) Pass() uint16 {
return b.pass
}
func (b *Breakpoint) SetPass(pass uint16) {
b.pass = pass
}
func (b *Breakpoint) IncPass() {
b.pass++
}
func (b *Breakpoint) Addr() uint16 {
return b.addr
}
func (b *Breakpoint) SetAddr(addr uint16) {
b.addr = addr
}
func (b *Breakpoint) Type() int {
return b.bpType
}
func (b *Breakpoint) SetType(bpType int) {
b.bpType = bpType
}
func cvtToUint16(v interface{}) (uint16, error) {
switch value := v.(type) {
case int:
return uint16(value), nil
case int8:
return uint16(value), nil
case int16:
return uint16(value), nil
case int32:
return uint16(value), nil
case int64:
return uint16(value), nil
case uint:
return uint16(value), nil
case uint8:
return uint16(value), nil
case uint16:
return value, nil
case uint32:
return uint16(value), nil
case uint64:
return uint16(value), nil
default:
// log.Errorf("Unknown type of %v", value)
return 0, fmt.Errorf("error: unsupported type of %v", value)
}
}
func getUint16(name string, ctx map[string]interface{}) uint16 {
if v, ok := ctx[name]; ok {
if v == nil {
return 0
}
// most frequent case
if v, ok := v.(uint16); ok {
return v
}
// for less frequent cases
rv, e := cvtToUint16(v)
if e != nil {
log.Errorf("error: variable %s have unsupported type: %v", name, v)
}
return rv
}
log.Errorf("Variable %s not found in context!", name)
return 0
}
func (b *Breakpoint) Hit(parameters map[string]interface{}) bool {
if !b.enabled {
return false
}
if b.bpType == BPTypeSimplePC {
pc := getUint16("PC", parameters)
if pc == b.addr {
log.Debugf("Breakpoint Hit PC=0x%04X", b.addr)
}
return pc == b.addr
} else if b.bpType == BPTypeSimpleSP {
sp := getUint16("SP", parameters)
if sp >= b.addr {
log.Debugf("Breakpoint Hit SP>=0x%04X", b.addr)
}
return sp >= b.addr
}
bc := context.WithValue(context.Background(), "MEM", parameters["MEM"])
bc = context.WithValue(bc, "IO", parameters["IO"])
value, err := b.eval.EvalBool(bc, parameters)
if err != nil {
fmt.Println(err)
}
return value
}
var language gval.Language
func init() {
language = gval.NewLanguage(gval.Base(), gval.Arithmetic(), gval.Bitmask(), gval.PropositionalLogic(),
gval.Function("PEEKW", CfPeekW),
gval.Function("PEEK", CfPeek),
gval.Function("BYTE", CfByte),
gval.Function("WORD", CfWord),
gval.Function("ABS", CfAbs),
gval.Function("IN", CfIn),
)
}
func (b *Breakpoint) SetExpression(expression string) error {
var err error
b.eval, err = language.NewEvaluable(expression)
if err != nil {
log.Error("Illegal expression", err)
return err
}
b.bpType = BPTypeExpression
return nil
}
func (b *Breakpoint) Expression() string {
return b.cond
}
func (b *Breakpoint) MBank() uint8 {
return b.mBank
}
func CfPeekW(ctx context.Context, arguments ...interface{}) (uint16, error) {
if len(arguments) != 1 {
return 0, fmt.Errorf("expected 1 argument")
}
addr, e := cvtToUint16(arguments[0])
if e != nil {
return 0, fmt.Errorf("error, illegal address: %v", e)
}
memInt := ctx.Value("MEM")
memRead, ok := memInt.(func(uint16) uint8)
if !ok {
return 0, fmt.Errorf("MEM must be func(uint16) uint8")
}
r := uint16(memRead(addr+1))<<8 | uint16(memRead(addr))
log.Debugf("PeekW[0x%04X]=0x%04X", addr, r)
return r, nil
}
func CfPeek(ctx context.Context, arguments ...interface{}) (uint16, error) {
result, err := CfPeekW(ctx, arguments...)
return result & 0x00ff, err
}
func CfIn(ctx context.Context, arguments ...interface{}) (uint16, error) {
if len(arguments) != 1 {
return 0, fmt.Errorf("expected 1 argument")
}
addr, e := cvtToUint16(arguments[0])
if e != nil {
return 0, fmt.Errorf("error, illegal address: %v", e)
}
ioInt := ctx.Value("IO")
ioRead, ok := ioInt.(func(uint16) uint8)
if !ok {
return 0, fmt.Errorf("IO must be func(uint16) uint8")
}
r := uint16(ioRead(addr))
log.Debugf("IO[0x%04X]=0x%02X", addr, r)
return r, nil
}
func CfByte(ctx context.Context, arguments ...interface{}) (uint8, error) {
if len(arguments) != 1 {
return 0, fmt.Errorf("expected 1 argument")
}
val, e := cvtToUint16(arguments[0])
if e != nil {
return 0, fmt.Errorf("error, illegal value: %v", e)
}
return uint8(val), nil
}
func CfWord(ctx context.Context, arguments ...interface{}) (uint16, error) {
if len(arguments) != 1 {
return 0, fmt.Errorf("expected 1 argument")
}
val, e := cvtToUint16(arguments[0])
if e != nil {
return 0, fmt.Errorf("error, illegal value: %v", e)
}
return val, nil
}
func CfAbs(ctx context.Context, arguments ...interface{}) (uint16, error) {
if len(arguments) != 1 {
return 0, fmt.Errorf("expected 1 argument")
}
val, e := cvtToUint16(arguments[0])
if e != nil {
return 0, fmt.Errorf("error, illegal value: %v", e)
}
if val < 256 {
// byte
i8 := int8(val)
if i8 < 0 {
return uint16(-i8), nil
}
return uint16(i8), nil
}
i16 := int16(val)
if i16 < 0 {
return uint16(-i16), nil
}
return uint16(i16), nil
}
//"IN(e): returns the byte at port e\n" \
//"NOT(e): negates expression e: if it's 0, returns 1. Otherwhise, return 0\n" \
//"ABS(e): returns absolute value of expression e\n" \
//"BYTE(e): same as (e)&FFH\n" \
//"WORD(e): same as (e)&FFFFH\n" \

View File

@ -1,87 +0,0 @@
package breakpoint
import (
"regexp"
"testing"
)
const expr1 = "PC=00100h and (NOT(B > 10)) and a != 5"
const expr2 = "PC=00115h"
const expr3 = "SP>=1332"
var ctx = map[string]interface{}{
"PC": 0x100,
"A": 0x55,
"B": 5,
"SP": 0x200,
}
const exprRep = "PC=00115h and (B=5 or BC = 5)"
const exprDst = "PC==0x00115 && (B==5 || BC == 5)"
const exprFn = "PC=PEEKW(SP-2) AND SP>=100"
func Test_PatchExpression(t *testing.T) {
ex := patchExpression(exprRep)
if ex != exprDst {
t.Errorf("Patched expression does not match\n got: %s\nexpected: %s", ex, exprDst)
}
}
func Test_ComplexExpr(t *testing.T) {
b, e := NewBreakpoint(expr1, 1)
//e := b.SetExpression(exp1)
if e != nil {
t.Error(e)
} else if b != nil {
b.enabled = true
if !b.Hit(ctx) {
t.Errorf("Breakpoint not hit")
}
}
}
const expSimplePC = "PC=00119h"
func Test_BPSetPC(t *testing.T) {
b, e := NewBreakpoint(expSimplePC, 1)
if e != nil {
t.Error(e)
} else if b != nil {
if b.bpType != BPTypeSimplePC {
t.Errorf("Breakpoint type does not match BPTypeSimplePC")
}
b.enabled = true
if b.Hit(ctx) {
t.Errorf("Breakpoint hit but will not!")
}
}
}
func Test_MatchSP(t *testing.T) {
pcMatch := regexp.MustCompile(`SP>=[[:xdigit:]]+$`)
matched := pcMatch.MatchString(expr3)
if !matched {
t.Errorf("SP>=XXXXh not matched")
}
}
func Test_GetCtx(t *testing.T) {
pc := getUint16("PC", ctx)
if pc != 0x100 {
t.Errorf("PC value not found in context")
}
}
func Test_PeekWFn(t *testing.T) {
b, e := NewBreakpoint(exprFn, 1)
if e != nil {
t.Error(e)
}
b.enabled = true
b.Hit(ctx)
}

View File

@ -1,305 +0,0 @@
package debug
import (
"fmt"
"okemu/debug/breakpoint"
"github.com/romychs/z80go"
"github.com/romychs/z80go/dis"
log "github.com/sirupsen/logrus"
)
const BPMemAccess = 65535
type Debugger struct {
stepMode bool
doStep bool
runMode bool
runInst uint64
breakpointsEnabled bool
breakpoints map[uint16]*breakpoint.Breakpoint
cpuFrequency uint32
disassembler *dis.Disassembler
cpuHistoryEnabled bool
cpuHistoryStarted bool
cpuHistoryMaxSize int
cpuHistory []*z80go.CPU
memBreakpoints [65536]byte
}
func NewDebugger() *Debugger {
d := Debugger{
stepMode: false,
doStep: false,
runMode: false,
runInst: 0,
breakpointsEnabled: false,
breakpoints: map[uint16]*breakpoint.Breakpoint{},
cpuHistoryEnabled: false,
cpuHistoryStarted: false,
cpuHistoryMaxSize: 0,
cpuHistory: []*z80go.CPU{},
}
return &d
}
type DEZOG interface {
SetupTcpHandler()
BreakpointHit(number uint16, typ byte)
}
func (d *Debugger) SetStepMode(step bool) {
d.SetRunMode(false)
d.stepMode = step
}
func (d *Debugger) SetRunMode(run bool) {
if run {
d.runInst = 0
}
d.runMode = run
}
func (d *Debugger) RunMode() bool {
return d.runMode
}
func (d *Debugger) DoStep() bool {
if d.doStep {
d.doStep = false
return true
}
return false
}
func (d *Debugger) SetCpuHistoryEnabled(enable bool) {
d.cpuHistoryEnabled = enable
}
func (d *Debugger) SetCpuHistoryMaxSize(size int) {
if size < 0 || size > 1_000_000 {
log.Error("CPU history max size must be positive and up to 1M")
} else {
d.cpuHistoryMaxSize = size
}
}
func (d *Debugger) CpuHistoryClear() {
d.cpuHistory = make([]*z80go.CPU, 0)
}
func (d *Debugger) CpuHistorySize() int {
return len(d.cpuHistory)
}
func (d *Debugger) CpuHistory(index int) *z80go.CPU {
if index >= 0 && index < len(d.cpuHistory) {
return d.cpuHistory[index]
}
if len(d.cpuHistory) > 0 {
log.Warnf("CPU history index %d out of range [0:%d]", index, len(d.cpuHistory)-1)
} else {
log.Warn("CPU history is empty")
}
return nil
}
func (d *Debugger) SetCpuHistoryStarted(started bool) {
d.cpuHistoryStarted = started
}
func (d *Debugger) SaveHistory(state *z80go.CPU) {
if d.cpuHistoryEnabled && d.cpuHistoryMaxSize > 0 && d.cpuHistoryStarted {
d.cpuHistory = append([]*z80go.CPU{state}, d.cpuHistory...)
if len(d.cpuHistory) > d.cpuHistoryMaxSize {
d.cpuHistory = d.cpuHistory[0 : d.cpuHistoryMaxSize-1]
}
}
}
func (d *Debugger) CheckBreakpoints(ctx map[string]interface{}) (bool, uint16) {
if d.breakpointsEnabled && d.runMode {
for n, bp := range d.breakpoints {
if bp != nil && bp.Hit(ctx) {
// breakpoint hit
if bp.Pass() >= bp.PassCount() {
bp.SetPass(0)
d.runMode = false
return true, n
}
// increment breakpoint pass count
bp.IncPass()
}
}
}
return false, 0
}
func (d *Debugger) SetBreakpointsEnabled(enabled bool) {
d.breakpointsEnabled = enabled
}
func (d *Debugger) BreakpointsEnabled() bool {
return d.breakpointsEnabled
}
// SetBreakpoint Create new breakpoint with specified number
func (d *Debugger) SetBreakpoint(number uint16, exp string, mBank uint8) error {
var err error
bp, err := breakpoint.NewBreakpoint(exp, mBank)
if err == nil && bp != nil {
d.breakpoints[number] = bp
}
return err
}
func (d *Debugger) AddBreakpoint(exp string, mBank uint8) (uint16, error) {
var err error
bpNo := d.GetBreakpointNum()
if bpNo < breakpoint.MaxBreakpoints {
bp, err := breakpoint.NewBreakpoint(exp, mBank)
if err == nil && bp != nil {
bp.SetEnabled(true)
d.breakpoints[bpNo] = bp
}
}
return bpNo, err
}
func (d *Debugger) GetBreakpointNum() uint16 {
num := uint16(1)
for no, bp := range d.breakpoints {
if bp != nil && no < breakpoint.MaxBreakpoints && num <= no {
num = no + 1
}
}
return num
}
func (d *Debugger) SetBreakpointPassCount(number uint16, count uint16) {
bp, ok := d.breakpoints[number]
if ok && bp != nil {
bp.SetPass(0)
bp.SetPassCount(count)
}
}
func (d *Debugger) SetBreakpointEnabled(number uint16, enabled bool) {
bp, ok := d.breakpoints[number]
if ok && bp != nil {
bp.SetEnabled(enabled)
}
}
func (d *Debugger) BreakpointEnabled(number uint16) bool {
bp, ok := d.breakpoints[number]
if ok && bp != nil {
return bp.Enabled()
}
return false
}
func (d *Debugger) BreakpointMBank(number uint16) uint8 {
bp, ok := d.breakpoints[number]
if ok && bp != nil {
return bp.MBank()
}
return 1
}
func (d *Debugger) ClearMemBreakpoints() {
for c := 0; c < 65536; c++ {
d.memBreakpoints[c] = 0
}
}
func (d *Debugger) StepMode() bool {
return d.stepMode
}
func (d *Debugger) SetDoStep(on bool) {
d.doStep = on
}
// BPExpression Return requested breakpoint
func (d *Debugger) BPExpression(number uint16) string {
bp, ok := d.breakpoints[number]
if ok && bp != nil {
return bp.Expression()
}
return ""
}
// RunInst return and increment count of instructions executed
func (d *Debugger) RunInst() uint64 {
v := d.runInst
d.runInst++
return v
}
func (d *Debugger) SetMemBreakpoint(address uint16, typ byte, size uint16) {
var offset uint16
for offset = address; offset < address+size; offset++ {
d.memBreakpoints[offset] = typ
}
}
func (d *Debugger) RemoveBreakpoint(number uint16) {
delete(d.breakpoints, number)
}
func (d *Debugger) CheckMemBreakpoints(accessMap *map[uint16]byte) (bool, uint16, byte) {
if !d.breakpointsEnabled {
return false, 0, 0
}
for addr, typ := range *accessMap {
bp := d.memBreakpoints[addr]
if bp == 0 {
return false, addr, 0
}
if (bp == 3) || bp == typ {
d.SetRunMode(false)
return true, addr, typ
}
}
return false, 0, 0
}
func (d *Debugger) ClearBreakpoints() {
clear(d.breakpoints)
}
type MemBP struct {
addr uint16
size uint16
}
func (d *Debugger) GetMemBreakpoints() []MemBP {
var res []MemBP
a := uint16(0)
s := uint16(0)
isBp := false
for addr := 0; addr < 65536; addr++ {
if d.memBreakpoints[addr] > 0 {
if !isBp {
isBp = true
a = uint16(addr)
}
s++
if addr == 65535 {
res = append(res, MemBP{addr: a, size: s})
}
} else {
if isBp {
isBp = false
res = append(res, MemBP{addr: a, size: s})
s = 0
}
}
}
return res
}
func (m *MemBP) String() string {
return fmt.Sprintf("%04XH : %d", m.addr, m.size)
}

View File

@ -1,556 +0,0 @@
package dzrp
import (
"bufio"
"errors"
"fmt"
"io"
"net"
"okemu/config"
"okemu/debug"
"okemu/debug/breakpoint"
"okemu/okean240"
"okemu/z80/dis"
"strconv"
log "github.com/sirupsen/logrus"
)
type DZRP struct {
port string
config *config.OkEmuConfig
debugger *debug.Debugger
disassembler *dis.Disassembler
computer *okean240.ComputerType
conn net.Conn
reader *bufio.Reader
writer *bufio.Writer
cmd *Command
}
// SetupTcpHandler Setup TCP listener, handle connections
func NewDZRP(config *config.OkEmuConfig, debug *debug.Debugger, dissasm *dis.Disassembler, comp *okean240.ComputerType) *DZRP {
return &DZRP{
port: config.Debugger.Host + ":" + strconv.Itoa(config.Debugger.Port),
debugger: debug,
disassembler: dissasm,
computer: comp,
}
}
func (p *DZRP) SetupTcpHandler() {
var err error
var l net.Listener
l, err = net.Listen("tcp4", p.port)
if err != nil {
fmt.Println(err)
return
}
defer func(l net.Listener) {
err := l.Close()
if err != nil {
log.Warnf("Error closing listener connection %v", err)
}
}(l)
log.Infof("Ready for debugger connections on %s", p.port)
for {
p.conn, err = l.Accept()
if err != nil {
log.Errorf("Accept connection: %v", err)
return
}
go p.handleConnection()
}
}
// Receive messages, split to strings and parse
func (p *DZRP) handleConnection() {
p.reader = bufio.NewReader(p.conn)
p.writer = bufio.NewWriter(p.conn)
var command Command
n := 0
for {
// receive command packet byte by byte
b, err := p.reader.ReadByte()
if err != nil {
if err == io.EOF {
break
} else {
log.Errorf("TCP error: %v", err)
p.debugger.SetStepMode(false)
return
}
}
switch n {
case 0:
command.Len = uint32(b)
case 1:
command.Len |= uint32(b) << 8
case 2:
command.Len |= uint32(b) << 16
case 3:
command.Len |= uint32(b) << 24
case 4:
command.Sn = b
case 5:
command.Id = b
default:
command.Payload = append(command.Payload, b)
}
if n >= 5 && int(command.Len) == len(command.Payload) {
p.cmd = &command
if p.handleCommand() {
break
}
command.Len = 0
command.Payload = []uint8{}
n = 0
} else {
n++
}
}
log.Debug("Closing connection")
p.debugger.SetStepMode(false)
_ = p.writer.Flush()
p.writer = nil
p.reader = nil
err := p.conn.Close()
if err != nil {
log.Warnf("Can not close TCP socket: %v", err)
}
}
// writeResponse Write response to command back to client
func (p *DZRP) writeResponse(response *Response) error {
// log.Infof("Ready for debugger connections on %s", port)
// Send Len
l := response.Len
for c := 0; c < 4; c++ {
e := p.writer.WriteByte(byte(l))
if e != nil {
log.Warnf("Error writing Len: %v", e)
}
l = l >> 8
}
// Send Sn
e := p.writer.WriteByte(response.Sn)
if e != nil {
log.Warnf("Error writing Sn: %v", e)
return e
}
// Send Payload
for _, b := range response.Payload {
e := p.writer.WriteByte(b)
if e != nil {
log.Warnf("Error writing payload: %v", e)
return e
}
}
e = p.writer.Flush()
if e != nil {
log.Warnf("Error flushing response: %v", e)
return e
}
return nil
}
// BreakpointHit Send notification message to Dezog on BP Hit
func (p *DZRP) BreakpointHit(number uint16, typ byte) {
// turn off temporary breakpoints on any hit
p.debugger.SetBreakpointEnabled(breakpoint.BpTmp1, false)
p.debugger.SetBreakpointEnabled(breakpoint.BpTmp2, false)
if p.writer != nil {
err := p.writeResponse(p.buildBpHitResponse(number, typ))
if err != nil {
log.Warnf("NTF_PAUSE, write response err: %v", err)
}
} else {
log.Warn("NTF_PAUSE, writer is nil")
}
}
func (p *DZRP) buildBpHitResponse(number uint16, typ byte) *Response {
bpReason, bpAddr, mBank := p.getBpReasonAndAddr(number, typ)
rsn := getBpReason(bpReason, bpAddr)
// msg - ASCIIZ bytes
msg := []byte(rsn)
msg = append(msg, byte(0))
rep := []byte{
1, // NTF_PAUSE
bpReason,
byte(bpAddr), byte(bpAddr >> 8),
mBank, // bank
}
rep = append(rep, msg...)
log.Debugf("NTF_PAUSE %s resp: %v", rsn, PayloadToString(rep))
return &Response{
Len: uint32(len(rep) + 1),
Sn: 0,
Payload: rep,
}
}
// handleCommand Handle command received from client, return true if last command is CMD_CLOSE
func (p *DZRP) handleCommand() bool {
log.Debugf("Handling command: %s", p.cmd.toString())
var err error
var resp *Response
handler, ok := commandHandlers[int(p.cmd.Id)]
if ok {
resp, err = handler(p)
} else {
//resp = NewResponse(p.cmd, nil)
err = errors.New("unknown command Id: " + strconv.Itoa(int(p.cmd.Id)))
}
if err == nil {
err = p.writeResponse(resp)
}
if err != nil {
log.Errorf("Error handling command: %v", err)
}
return p.cmd.Id == CMD_CLOSE
}
type CommandHandler func(*DZRP) (*Response, error)
var commandHandlers = map[int]CommandHandler{
CMD_INIT: (*DZRP).handleCmdInit, //1
CMD_CLOSE: (*DZRP).handleCmdClose, //2
CMD_GET_REGISTERS: (*DZRP).handleCmdGetRegisters, //3
CMD_SET_REGISTER: (*DZRP).handleCmdSetRegister, //4
CMD_CONTINUE: (*DZRP).handleCmdContinue, //6
CMD_PAUSE: (*DZRP).handleCmdPause, // 7
CMD_READ_MEM: (*DZRP).handleCmdReadMem, //8
CMD_WRITE_MEM: (*DZRP).handleCmdWriteMem, //9
CMD_LOOPBACK: (*DZRP).handleCmdLoopback, // 15
CMD_READ_PORT: (*DZRP).handleCmdReadPort, // 20
CMD_WRITE_PORT: (*DZRP).handleCmdWritePort, // 21
CMD_INTERRUPT_ON_OFF: (*DZRP).handleCmdInterruptOnOff, // 23
CMD_ADD_BREAKPOINT: (*DZRP).handleCmdAddBreakpoint, //40
CMD_REMOVE_BREAKPOINT: (*DZRP).handleCmdRemoveBreakpoint, //41
}
func (p *DZRP) handleCmdInit() (*Response, error) {
if len(p.cmd.Payload) < 4 {
return nil, errors.New("too short payload")
}
p.debugger.SetStepMode(true)
p.debugger.SetBreakpointsEnabled(true)
p.debugger.ClearBreakpoints()
p.debugger.ClearMemBreakpoints()
app := string(p.cmd.Payload[3 : len(p.cmd.Payload)-1])
log.Debugf("CMD_INIT: client version %d.%d.%d App: %s", p.cmd.Payload[0], p.cmd.Payload[1], p.cmd.Payload[2], app)
payload := []byte{0, VersionMajor, VersionMinor, VersionPatch, MachineZX128K}
payload = append(payload, []byte(AppName)...)
payload = append(payload, 0)
return NewResponse(p.cmd, payload), nil
}
func (p *DZRP) handleCmdClose() (*Response, error) {
log.Debug("CMD_CLOSE")
return NewResponse(p.cmd, nil), nil
}
func (p *DZRP) handleCmdPause() (*Response, error) {
log.Debug("CMD_PAUSE")
p.debugger.SetStepMode(true)
//return NewResponse(p.cmd, []byte{p.cmd.Sn}), nil
return NewResponse(p.cmd, nil), nil
}
func (p *DZRP) handleCmdReadMem() (*Response, error) {
if len(p.cmd.Payload) < 5 {
return nil, errors.New("too short payload")
}
addr := uint16(p.cmd.Payload[2])<<8 + uint16(p.cmd.Payload[1])
size := uint16(p.cmd.Payload[4])<<8 + uint16(p.cmd.Payload[3])
log.Debugf("CMD_READ_MEM[0x%04X] len: 0x%04X", addr, size)
mem := make([]byte, size)
//mem[0] = cmd.Sn
for i := 0; i < int(size); i++ {
mem[i] = p.computer.MemRead(addr)
addr++
}
return NewResponse(p.cmd, mem), nil
}
func (p *DZRP) handleCmdWriteMem() (*Response, error) {
if len(p.cmd.Payload) < 4 {
return nil, errors.New("too short payload")
}
addr := uint16(p.cmd.Payload[1]) + uint16(p.cmd.Payload[2])<<8
log.Debugf("CMD_WRITE_MEM[0x%04X] len: 0x%04X", addr, len(p.cmd.Payload)-3)
for i := 3; i < len(p.cmd.Payload); i++ {
p.computer.MemWrite(addr, p.cmd.Payload[i])
addr++
}
return NewResponse(p.cmd, []byte{p.cmd.Sn}), nil
}
func (p *DZRP) handleCmdSetRegister() (*Response, error) {
lo := p.cmd.Payload[1]
hi := p.cmd.Payload[2]
word := uint16(hi)<<8 | uint16(lo)
s := p.computer.CPUState()
switch p.cmd.Payload[0] {
case RegPC:
s.PC = word
log.Debugf("Set PC=0x%04X", s.PC)
case RegSP:
s.SP = word
log.Debugf("Set SP=0x%04X", s.PC)
case RegAF:
s.A = hi
s.Flags.SetFlags(lo)
case RegBC:
s.B = hi
s.C = lo
case RegDE:
s.D = hi
s.E = lo
case RegHL:
s.H = hi
s.L = lo
case RegIX:
s.IX = word
case RegIY:
s.IY = word
case RegAF_:
s.AAlt = hi
s.FlagsAlt.SetFlags(lo)
case RegBC_:
s.BAlt = hi
s.CAlt = lo
case RegDE_:
s.DAlt = hi
s.EAlt = lo
case RegHL_:
s.HAlt = hi
s.LAlt = lo
case RegIM:
s.IMode = lo
case RegF:
s.Flags.SetFlags(lo)
case RegA:
s.A = lo
case RegC:
s.C = lo
case RegB:
s.B = lo
case RegE:
s.E = lo
case RegD:
s.D = lo
case RegL:
s.L = lo
case RegH:
s.H = lo
case RegIXL:
s.IX = s.IX&0xff00 | uint16(lo)
case RegIXH:
s.IX = (uint16(lo) << 8) | (s.IX & 0x00ff)
case RegIYL:
s.IY = s.IY&0xff00 | uint16(lo)
case RegIYH:
s.IY = (uint16(lo) << 8) | (s.IY & 0x00ff)
case RegF_:
s.FlagsAlt.SetFlags(lo)
case RegA_:
s.AAlt = lo
case RegC_:
s.CAlt = lo
case RegB_:
s.BAlt = lo
case RegE_:
s.EAlt = lo
case RegD_:
s.DAlt = lo
case RegL_:
s.LAlt = lo
case RegH_:
s.HAlt = lo
case RegR:
s.R = lo
case RegI:
s.I = lo
default:
return nil, errors.New("unknown register no: " + strconv.Itoa(int(p.cmd.Payload[0])))
}
p.computer.SetCPUState(s)
//return NewResponse(p.cmd, []byte{p.cmd.Sn}), nil
return NewResponse(p.cmd, nil), nil
}
func (p *DZRP) handleCmdGetRegisters() (*Response, error) {
s := p.computer.CPUState()
resp := []byte{
//p.cmd.Sn,
byte(s.PC), byte(s.PC >> 8),
byte(s.SP), byte(s.SP >> 8),
s.Flags.GetFlags(), s.A,
s.C, s.B,
s.E, s.D,
s.L, s.H,
byte(s.IX), byte(s.IX >> 8),
byte(s.IY), byte(s.IY >> 8),
s.FlagsAlt.GetFlags(), s.AAlt,
s.CAlt, s.BAlt,
s.EAlt, s.DAlt,
s.LAlt, s.HAlt,
s.R,
s.I,
s.IMode,
0, // reserved
0, // Nslots. The number of slots that will follow.
}
log.Debugf("CMD_GET_REGISTERS resp: %v", resp)
return NewResponse(p.cmd, resp), nil
}
func (p *DZRP) handleCmdContinue() (*Response, error) {
eb1 := p.cmd.Payload[0] != 0
ab1 := (uint16(p.cmd.Payload[2]) << 8) | uint16(p.cmd.Payload[1])
p.setTempBreakpoint(eb1, breakpoint.BpTmp1, ab1)
eb2 := p.cmd.Payload[3] != 0
ab2 := (uint16(p.cmd.Payload[5]) << 8) | uint16(p.cmd.Payload[4])
p.setTempBreakpoint(eb2, breakpoint.BpTmp2, ab2)
log.Debugf("CMD_CONTINUE BP1 en: %v, addr: 0x%04X; BP2 en: %v, addr: 0x%04X; AC: 0x%02X",
eb1, ab1, eb2, ab2, p.cmd.Payload[6])
p.debugger.SetRunMode(true)
//return NewResponse(p.cmd, []byte{p.cmd.Sn}), nil
return NewResponse(p.cmd, nil), nil
}
func (p *DZRP) setTempBreakpoint(ena bool, no uint16, addr uint16) {
if ena {
e := p.debugger.SetBreakpoint(no, fmt.Sprintf("PC=%04Xh", addr), 0)
if e != nil {
log.Debugf("setTmpBreakpoint err: %v", e)
}
p.debugger.SetBreakpointEnabled(no, true)
} else {
p.debugger.SetBreakpointEnabled(no, false)
}
}
func (p *DZRP) getBpReasonAndAddr(number uint16, typ byte) (reason byte, addr uint16, mBank uint8) {
reason = BprStepOver // default StepOver
addr = number
mBank = uint8(1)
if typ >= 1 && typ <= 2 { // 1-rd, 2-wr
reason = typ + 2
} else {
if number != breakpoint.BpTmp1 && number != breakpoint.BpTmp2 {
// bp hit
mBank = p.debugger.BreakpointMBank(addr)
reason = BprHit
}
addr = p.computer.CPUState().PC
}
return reason, addr, mBank
}
func (p *DZRP) handleCmdAddBreakpoint() (*Response, error) {
if len(p.cmd.Payload) < 4 {
return nil, errors.New("too short payload")
}
addr := (uint16(p.cmd.Payload[1]) << 8) | uint16(p.cmd.Payload[0])
mBank := p.cmd.Payload[2]
isCond := p.cmd.Payload[3] != 0
var expr string
if isCond {
expr = string(p.cmd.Payload[3 : len(p.cmd.Payload)-3])
} else {
expr = fmt.Sprintf("PC=%04Xh", addr)
}
log.Debugf("CMD_ADD_BREAKPOINT addr: 0x%04X, bank: %d, cond: '%s'", addr, mBank, expr)
bpNum, err := p.debugger.AddBreakpoint(expr, mBank)
if err != nil {
log.Debugf("SetBreakpoint err: %v", err)
return nil, err
}
if bpNum == breakpoint.MaxBreakpoints {
bpNum = 0
}
//bpNum := p.debugger.GetBreakpointNum()
//if bpNum < breakpoint.MaxBreakpoints {
// err := p.debugger.SetBreakpoint(bpNum, cond, mBank)
// p.debugger.SetBreakpointEnabled(bpNum, true)
// if err != nil {
// log.Debugf("SetBreakpoint err: %v", err)
// return nil, err
// }
//} else {
// bpNum = 0
//}
return NewResponse(p.cmd, []byte{byte(bpNum), byte(bpNum >> 8)}), nil
}
func getBpReason(reason byte, addr uint16) string {
rsn, ok := BprReasons[int(reason)]
if !ok {
rsn = strconv.Itoa(int(reason))
}
return fmt.Sprintf("BP: '%s', at: 0x%04X", rsn, addr)
}
func (p *DZRP) handleCmdRemoveBreakpoint() (*Response, error) {
if len(p.cmd.Payload) < 2 {
return nil, errors.New("too short payload")
}
bpNum := (uint16(p.cmd.Payload[1]) << 8) | uint16(p.cmd.Payload[0])
p.debugger.RemoveBreakpoint(bpNum)
log.Debugf("CMD_REMOVE_BREAKPOINT no: %d", bpNum)
return NewResponse(p.cmd, nil), nil
}
// handleCmdLoopback send back received data
func (p *DZRP) handleCmdLoopback() (*Response, error) {
return NewResponse(p.cmd, p.cmd.Payload), nil
}
func (p *DZRP) handleCmdReadPort() (*Response, error) {
if len(p.cmd.Payload) < 2 {
return nil, errors.New("too short payload")
}
addr := (uint16(p.cmd.Payload[1]) << 8) | uint16(p.cmd.Payload[0])
return NewResponse(p.cmd, []byte{p.computer.IORead(addr)}), nil
}
func (p *DZRP) handleCmdWritePort() (*Response, error) {
if len(p.cmd.Payload) < 3 {
return nil, errors.New("too short payload")
}
addr := (uint16(p.cmd.Payload[1]) << 8) | uint16(p.cmd.Payload[0])
p.computer.IOWrite(addr, p.cmd.Payload[2])
return NewResponse(p.cmd, nil), nil
}
func (p *DZRP) handleCmdInterruptOnOff() (*Response, error) {
if len(p.cmd.Payload) == 0 {
return nil, errors.New("too short payload")
}
on := p.cmd.Payload[0] != 0
p.debugger.SetBreakpointsEnabled(on)
log.Debugf("CMD_INTERRUPT_ONOFF on: %t", on)
return NewResponse(p.cmd, nil), nil
}

View File

@ -1,178 +0,0 @@
package dzrp
import "fmt"
const (
VersionMajor = 2
VersionMinor = 1
VersionPatch = 0
AppName = "okemu v1.0.0"
)
type Command struct {
Len uint32 // Length of the payload data. (little endian)
Sn uint8 // Sequence number, 1-255. Increased with each command
Id uint8 // Command ID
Payload []uint8 // Payload: Data[0]..Data[n-1]
}
type Notification struct {
Len uint32 // Length of the following data beginning with the sequence number. (little endian)
Sn uint8 // Sequence number = 0
Payload []uint8 // Payload: Data[0]..Data[n-1]
}
type Response struct {
Len uint32 // Length of the following data beginning with the sequence number. (little endian)
Sn uint8 // Sequence number, same as command.
Payload []uint8 // Payload: Data[0]..Data[n-1]
}
// The DRZP commands and responses.
// The response contains the command with the bit 7 set.
const (
CMD_INIT = 1
CMD_CLOSE = 2
CMD_GET_REGISTERS = 3
CMD_SET_REGISTER = 4
CMD_WRITE_BANK = 5
CMD_CONTINUE = 6
CMD_PAUSE = 7
CMD_READ_MEM = 8
CMD_WRITE_MEM = 9
CMD_SET_SLOT = 10
CMD_GET_TBBLUE_REG = 11
CMD_SET_BORDER = 12
CMD_SET_BREAKPOINTS = 13
CMD_RESTORE_MEM = 14
CMD_LOOPBACK = 15
CMD_GET_SPRITES_PALETTE = 16
CMD_GET_SPRITES_CLIP_WINDOW_AND_CONTROL = 17
// Sprites
CMD_GET_SPRITES = 18
CMD_GET_SPRITE_PATTERNS = 19
CMD_READ_PORT = 20
CMD_WRITE_PORT = 21
CMD_EXEC_ASM = 22
CMD_INTERRUPT_ON_OFF = 23
// Breakpoint
CMD_ADD_BREAKPOINT = 40
CMD_REMOVE_BREAKPOINT = 41
CMD_ADD_WATCHPOINT = 42
CMD_REMOVE_WATCHPOINT = 43
// State
CMD_READ_STATE = 50
CMD_WRITE_STATE = 51
)
// DZRP notifications.
const NTF_PAUSE = 1
// Machine type that is returned in CMD_INIT.
// It is required to determine the memory model
const (
MachineUnknown = 0
MachineZX16K = 1
MachineZX48K = 2
MachineZX128K = 3
MachineZXNEXT = 4
)
const (
RegPC = iota
RegSP
RegAF
RegBC
RegDE
RegHL
RegIX
RegIY
RegAF_
RegBC_
RegDE_
RegHL_
RegUnk
RegIM
RegF
RegA
RegC
RegB
RegE
RegD
RegL
RegH
RegIXL
RegIXH
RegIYL
RegIYH
RegF_
RegA_
RegC_
RegB_
RegE_
RegD_
RegL_
RegH_
RegR
RegI
)
type CmdInitCommand struct {
Major uint8 // Version (of the command sender): 3 bytes, big endian: Major.Minor.Patch
Minor uint8
Patch uint8
AppName string // 0-terminated string The program name + version as a string. E.g. "DeZog v1.4.0"
}
type CmdInitResponse struct {
Sn uint8 // Same seq no
Error uint8 // Error: 0=no error, 1=general (unknown) error.
Major uint8 // Version (of the response sender) : 3 bytes, big endian: Major.Minor.Patch
Minor uint8
Patch uint8
Machine uint8 // Machine type (memory model): 0 = UNKNOWN, 1 = ZX16K, 2 = ZX48K, 3 = ZX128K, 4 = ZXNEXT.
AppName string // 0-terminated string The responding program name + version as a string. E.g. "dbg_uart_if v2.0.0"}
}
const (
BprStepOver = 0
BprManual = 1
BprHit = 2
BprMemRead = 3
BprMemWrite = 4
BprOther = 255
)
var BprReasons = map[int]string{
BprStepOver: "Step-over",
BprManual: "Manual break",
BprHit: "Hit",
BprMemRead: "WP read",
BprMemWrite: "WP Write",
BprOther: "Other",
}
func (c *Command) toString() string {
return fmt.Sprintf("Len: %d, Sn: %d, Id: %d, Payload: %s", c.Len, c.Sn, c.Id, PayloadToString(c.Payload))
}
func NewResponse(cmd *Command, payload []byte) *Response {
return &Response{
Len: uint32(len(payload) + 1),
Sn: cmd.Sn,
Payload: payload,
}
}
func PayloadToString(payload []byte) string {
res := "["
for _, b := range payload {
res += fmt.Sprintf("%02X ", b)
}
return res + "]"
}

View File

@ -1,149 +0,0 @@
package debug
import (
"errors"
"strconv"
"strings"
)
// Operators with their priority
var operators = map[string]int{
"(": 12, ")": 12,
"*": 11, "/": 11, "%": 11,
"+": 10, "-": 10,
"<<": 9, ">>": 9,
"<": 8, "<=": 8, ">": 8, ">=": 8,
"=": 7, "!=": 7,
"&": 6,
"^": 5,
"|": 4,
"&&": 3,
"||": 2,
}
var variables = map[string]bool{
"A": true, "B": true, "C": true, "D": true, "E": true, "F": true, "H": true, "L": true, "I": true,
"R": true, "SF": true, "NF": true, "PF": true, "VF": true, "XF": true, "YF": true, "ZF": true,
"A'": true, "B'": true, "C'": true, "D'": true, "E'": true, "F'": true, "H'": true, "L'": true,
"AF": true, "BC": true, "DE": true, "HL": true, "IX": true, "IY": true, "PC": true, "SP": true,
}
const (
OTUnknown = iota
OTValue
OTVariable
OTOperation
)
type Token struct {
name string
val uint16
ot int
}
type Expression struct {
infixExp string
inStack []Token
outStack []Token
}
func NewExpression(infixExp string) *Expression {
return &Expression{infixExp, make([]Token, 0), make([]Token, 0)}
}
func (e *Expression) Parse() error {
e.infixExp = strings.ToUpper(strings.TrimSpace(e.infixExp))
if e.infixExp == "" {
return errors.New("no Expression")
}
ptr := 0
for ptr < len(e.infixExp) {
token, err := getNextToken(e.infixExp[ptr:])
if err != nil {
return err
}
err = validate(token)
if err != nil {
return err
}
err = e.parseToken(token)
if err != nil {
return err
}
ptr += len(token.name)
}
return nil
}
func (e *Expression) parseToken(token Token) error {
return nil
}
func validate(token Token) error {
switch token.ot {
case OTValue:
v, err := strconv.ParseUint(token.name, 0, 16)
if err != nil {
return err
}
token.val = uint16(v)
case OTVariable:
if !variables[token.name] {
return errors.New("unknown variable")
}
case OTOperation:
v, ok := operators[token.name]
if !ok {
return errors.New("unknown operation")
}
token.val = uint16(v)
default:
return errors.New("unknown token")
}
return nil
}
const operations = "*/%+_<=>!&^|"
func getNextToken(str string) (Token, error) {
ptr := 0
exp := ""
ot := OTUnknown
for ptr < len(str) {
ch := str[ptr]
if ch == ' ' {
if ot == OTUnknown {
ptr++
continue
} else {
// end of token
return Token{name: exp, ot: ot}, nil
}
}
if (ch == 'X' || ch == 'O' || ch == 'B' || ch == 'H') && ot != OTValue {
exp += string(ch)
ptr++
continue
}
if ch >= '0' && ch <= '9' {
if len(exp) == 0 {
ot = OTValue
}
exp += string(ch)
ptr++
continue
}
if strings.Contains(operations, string(ch)) {
if len(exp) == 0 {
ot = OTOperation
}
exp += string(ch)
ptr++
continue
}
return Token{name: exp, ot: ot}, errors.New("invalid token")
}
return Token{name: exp, ot: ot}, nil
}

View File

@ -1,23 +0,0 @@
package zrcp
const welcomeMessage = "Welcome to Ocean-240.2 remote command protocol (ZRCP partial implementation)\nWrite help for available commands\n"
const emptyResponse = "\ncommand> "
const aboutResponse = "ZEsarUX remote command protocol"
const getVersionResponse = "12.1"
const getRegistersResponse = "PC=%04x SP=%04x AF=%04x BC=%04x HL=%04x DE=%04x IX=%04x IY=%04x AF'=%04x BC'=%04x HL'=%04x DE'=%04x I=%02x R=%02x F=%s F'=%s MEMPTR=%04x IM0 IFF%s VPS: 0 MMU=%s"
const getStateResponse = "PC=%04x SP=%04x AF=%04x BC=%04x HL=%04x DE=%04x IX=%04x IY=%04x AF'=%04x BC'=%04x HL'=%04x DE'=%04x I=%02x R=%02x IM0 IFF%s (PC)=%s (SP)=%s MMU=%s"
const inCpuStepResponse = "\ncommand@cpu-step> "
const getMachineResponse = "Ocean-240.2 64K\n"
const respErrorLoading = "ERROR loading file"
const quitResponse = "Sayonara baby\n"
const runUntilBPMessage = "Running until a breakpoint, key press or data sent, menu opening or other event\n"
var PushValueTypeName = []string{
"default",
"call",
"rst",
"push",
"maskable_interrupt",
"non_maskable_interrupt",
}

View File

@ -1,56 +0,0 @@
package zrcp
import (
"fmt"
"strconv"
"strings"
)
func parseUint64(s string) (uint64, error) {
v := strings.TrimSpace(strings.ToUpper(s))
base := 0
if strings.HasSuffix(v, "H") {
v = strings.TrimSuffix(v, "H")
base = 16
}
a, e := strconv.ParseUint(v, base, 64)
return a, e
}
func parseUint16(s string) (uint16, error) {
v, e := parseUint64(s)
if v > 65535 {
return uint16(v), fmt.Errorf("too big uint16: %d", v)
}
return uint16(v), e
}
func toW(hi, lo byte) uint16 {
return uint16(lo) | (uint16(hi) << 8)
}
func iifStr(iif1, iif2 bool) string {
flags := []byte{'-', '-'}
if iif1 {
flags[0] = '1'
}
if iif2 {
flags[1] = '2'
}
return string(flags)
}
func typToString(typ uint8) string {
switch typ {
case 0:
return "D"
case 1:
return "R"
case 2:
return "W"
case 3:
return "R/W"
default:
return "x"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +0,0 @@
package zrcp
import (
"testing"
)
const exp1 = "CS=0x0100 & SP>=256"
const memBpSet = "11ch 3 1"
func Test_SetMemPB(t *testing.T) {
//p := New
//resp := p.SetMemBreakpoint(memBpSet)
//if resp != "" {
// t.Errorf("SetMemBreakpoint() returned %s", resp)
//}
}

View File

@ -1,191 +0,0 @@
package forms
import (
"fmt"
"image/color"
"okemu/config"
"okemu/okean240"
"okemu/okean240/fdc"
"os"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/driver/desktop"
"fyne.io/fyne/v2/layout"
"fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
func NewMainWindow(computer *okean240.ComputerType, config *config.OkEmuConfig, title string) (*fyne.Window, *canvas.Raster, *widget.Label) {
emulatorApp := app.NewWithID("oceanemu-0001-0000-0001-8f33aba1fc0e")
w := emulatorApp.NewWindow(title)
// Handle all keys typed in main window canvas
w.Canvas().SetOnTypedKey(
func(key *fyne.KeyEvent) {
computer.PutKey(key)
})
w.Canvas().SetOnTypedRune(
func(key rune) {
computer.PutRune(key)
})
addShortcuts(w.Canvas(), computer)
// ---
label := widget.NewLabel(fmt.Sprintf("Screen size: %dx%d", computer.ScreenWidth(), computer.ScreenHeight()))
raster := canvas.NewRasterWithPixels(
func(x, y, w, h int) color.Color {
var xx uint16
if computer.ScreenWidth() == 512 {
xx = uint16(x)
} else {
xx = uint16(x) / 2
}
return computer.GetPixel(xx, uint16(y/2))
})
raster.Resize(fyne.NewSize(512, 512))
raster.SetMinSize(fyne.NewSize(512, 512))
centerRaster := container.NewCenter(raster)
w.Resize(fyne.NewSize(600, 600))
//vBox := container.NewVBox(
// newToolbar(computer, w, emulatorApp, config),
// centerRaster,
// label,
//)
vBox := container.NewBorder(
newToolbar(computer, w, emulatorApp, config),
label,
nil,
nil,
centerRaster,
)
w.SetContent(vBox)
return &w, raster, label
}
var floppyDriveExt = []string{".okd", ".dat", ".bin", ".raw"}
func newOkdOpenDialog(drive byte, c *okean240.ComputerType, w fyne.Window, config *config.OkEmuConfig) *dialog.FileDialog {
fod := dialog.NewFileOpen(func(reader fyne.URIReadCloser, err error) {
// Read file list error occurred
if err != nil {
dialog.ShowError(err, w)
return
}
// User Cancelled
if reader == nil {
return
}
// User select Open button, load data
err = c.LoadFloppyData(drive, reader)
if err != nil {
dialog.ShowError(err, w)
}
_ = reader.Close()
}, w)
fod.SetFileName(config.FDC[drive].FloppyFile)
cwd, e := os.Getwd()
if e == nil {
uri, e := storage.ListerForURI(storage.NewFileURI(cwd))
if e == nil {
fod.SetLocation(uri)
}
}
fod.SetTitleText(fmt.Sprintf("Load floppy %s: image", string(rune(int(drive+66)))))
fod.SetFilter(storage.NewExtensionFileFilter(floppyDriveExt))
fod.Resize(fyne.NewSize(580, 500))
return fod
}
func newToolbar(c *okean240.ComputerType, w fyne.Window, a fyne.App, config *config.OkEmuConfig) *fyne.Container {
hBox := container.NewHBox()
for d := 0; d < fdc.TotalDrives; d++ {
hBox.Add(widget.NewLabel(string(rune(66+d)) + ":"))
hBox.Add(widget.NewToolbar(
// Button Open
widget.NewToolbarAction(theme.FolderOpenIcon(), func() {
newOkdOpenDialog(byte(d), c, w, config).Show()
}),
// Button Save
widget.NewToolbarAction(theme.DocumentSaveIcon(), func() {
err := c.SaveFloppy(byte(d))
if err != nil {
dialog.ShowError(err, w)
}
}),
))
hBox.Add(widget.NewSeparator())
}
//hBox.Add(widget.NewButtonWithIcon("1", theme.DownloadIcon(), func() {
// c.SetRamBytes(ramBytes)
//}))
//
//hBox.Add(widget.NewSeparator())
hBox.Add(widget.NewButtonWithIcon("^C", theme.MediaStopIcon(), func() {
c.PutCtrlKey(0x03)
}))
hBox.Add(widget.NewSeparator())
bNorm := widget.NewButtonWithIcon("", theme.MediaPlayIcon(), nil)
bFast := widget.NewButtonWithIcon("", theme.MediaFastForwardIcon(), nil)
bFast.OnTapped = func() {
c.SetFullSpeed(true)
c.SetCPUFrequency(50_000_000)
bNorm.Enable()
bFast.Disable()
}
bNorm.OnTapped = func() {
c.SetFullSpeed(false)
c.SetCPUFrequency(2_500_000)
bNorm.Disable()
bFast.Enable()
}
bNorm.Disable()
//hBox.Add(cbFreq)
hBox.Add(bNorm)
hBox.Add(bFast)
hBox.Add(widget.NewSeparator())
hBox.Add(widget.NewButtonWithIcon("", theme.SettingsIcon(), func() {
cfg := config.Clone()
d := dialog.NewCustomConfirm("OK-Emu settings", "Save", "Cancel", settingsDialog(cfg), func(b bool) {
if b {
cfg.Save()
}
}, w)
d.Resize(fyne.NewSize(450, 360))
//w.SetContent(settings.NewSettings().LoadAppearanceScreen(w))
d.Show()
}))
hBox.Add(layout.NewSpacer())
hBox.Add(widget.NewButtonWithIcon("Reset", theme.MediaReplayIcon(), func() {
c.SetPendingHardReset(true)
//computer.Reset(conf)
}))
return hBox
}
// Add shortcuts for all Ctrl+<Letter>
func addShortcuts(c fyne.Canvas, computer *okean240.ComputerType) {
// Add shortcuts for Ctrl+A to Ctrl+Z
for kName := 'A'; kName <= 'Z'; kName++ {
kk := fyne.KeyName(kName)
sc := &desktop.CustomShortcut{KeyName: kk, Modifier: fyne.KeyModifierControl}
c.AddShortcut(sc, func(shortcut fyne.Shortcut) { computer.PutCtrlKey(byte(kName&0xff) - 0x40) })
}
}

View File

@ -1,102 +0,0 @@
package forms
import (
"errors"
"okemu/config"
"okemu/okean240/fdc"
"os"
"strconv"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/data/binding"
"fyne.io/fyne/v2/data/validation"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
log "github.com/sirupsen/logrus"
)
func settingsDialog(config *config.OkEmuConfig) fyne.CanvasObject {
debugger := widget.NewForm(
widget.NewFormItem("Remote debug", debugCheckBox(config)),
widget.NewFormItem("Host", hostEntry(config)),
widget.NewFormItem("Port", portEntry(config)))
cont := container.NewAppTabs(
container.NewTabItemWithIcon("Debug", theme.MediaPlayIcon(), widget.NewCard("", "", debugger)),
)
for drv := byte(0); drv < fdc.TotalDrives; drv++ {
cont.Append(
container.NewTabItemWithIcon("Drive "+string(rune(66+drv))+":", theme.DocumentSaveIcon(), widget.NewCard("Floppy 720k", "", diskForm(config, drv))),
)
}
return cont
}
func portEntry(cfg *config.OkEmuConfig) *widget.Entry {
dbgPort := widget.NewEntry()
dbgPort.SetText(strconv.Itoa(cfg.Debugger.Port))
dbgPort.Validator = validation.NewRegexp(`^[0-9]+$`, "port can only contain numbers")
dbgPort.OnSubmitted = func(s string) {
p, e := strconv.Atoi(s)
if e != nil {
log.Warn("Illegal port number: " + s)
} else {
cfg.Debugger.Port = p
}
}
return dbgPort
}
func hostEntry(cfg *config.OkEmuConfig) *widget.Entry {
entry := widget.NewEntry()
entry.SetText(cfg.Debugger.Host)
entry.Validator = validation.NewRegexp(`^[A-Za-z0-9_-]+$`, "hostname can only contain letters, numbers, '_', and '-'")
entry.OnSubmitted = func(s string) {
cfg.Debugger.Host = s
}
return entry
}
func debugCheckBox(cfg *config.OkEmuConfig) *widget.Check {
// Debug Enabled
check := widget.NewCheck("Enable", func(checked bool) {
cfg.Debugger.Enabled = checked
})
check.Checked = cfg.Debugger.Enabled
return check
}
func diskForm(cfg *config.OkEmuConfig, drv byte) *widget.Form {
dskAutoLoad := widget.NewCheck("AutoLoad", func(checked bool) {
cfg.FDC[drv].AutoLoad = checked
})
dskAutoLoad.Checked = cfg.FDC[drv].AutoLoad
dskAutoSave := widget.NewCheck("AutoSave", func(checked bool) {
cfg.FDC[drv].AutoSave = checked
})
dskAutoSave.Checked = cfg.FDC[drv].AutoSave
dskFileName := widget.NewEntryWithData(binding.BindString(&cfg.FDC[drv].FloppyFile))
dskFileName.SetPlaceHolder("Enter file name...")
dskFileName.Validator = FileExists("File not exists")
return widget.NewForm(
widget.NewFormItem("AutoLoad", dskAutoLoad),
widget.NewFormItem("AutoSave", dskAutoSave),
widget.NewFormItem("File", dskFileName),
)
}
func FileExists(reason string) fyne.StringValidator {
return func(text string) error {
_, e := os.Stat(text)
if errors.Is(e, os.ErrNotExist) {
return errors.New(reason)
}
return nil
}
}

View File

@ -1,51 +0,0 @@
module okemu
go 1.25
require (
fyne.io/fyne/v2 v2.7.3
github.com/PaesslerAG/gval v1.2.4
github.com/PaesslerAG/jsonpath v0.1.0
github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6
github.com/loov/hrtime v1.0.4
github.com/shopspring/decimal v1.3.1
github.com/sirupsen/logrus v1.9.4
gopkg.in/yaml.v3 v3.0.1
)
require (
fyne.io/systray v1.12.0 // indirect
github.com/BurntSushi/toml v1.5.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.1 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fyne-io/gl-js v0.2.0 // indirect
github.com/fyne-io/glfw-js v0.3.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.2.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.3.3 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/romychs/z80go v1.0.1 // indirect
github.com/rymdport/portal v0.4.2 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.11.1 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
)

View File

@ -1,103 +0,0 @@
fyne.io/fyne/v2 v2.7.3 h1:xBT/iYbdnNHONWO38fZMBrVBiJG8rV/Jypmy4tVfRWE=
fyne.io/fyne/v2 v2.7.3/go.mod h1:gu+dlIcZWSzKZmnrY8Fbnj2Hirabv2ek+AKsfQ2bBlw=
fyne.io/systray v1.12.0 h1:CA1Kk0e2zwFlxtc02L3QFSiIbxJ/P0n582YrZHT7aTM=
fyne.io/systray v1.12.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/PaesslerAG/gval v1.2.4 h1:rhX7MpjJlcxYwL2eTTYIOBUyEKZ+A96T9vQySWkVUiU=
github.com/PaesslerAG/gval v1.2.4/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbVR+C6xac=
github.com/PaesslerAG/jsonpath v0.1.0 h1:gADYeifvlqK3R3i2cR5B4DGgxLXIPb3TRTH1mGi0jPI=
github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.3.3 h1:ihGNJU9KzdK2QRDy1Bm7FT5RFQoYb+3n3EIhI/4eaQc=
github.com/go-text/typesetting v0.3.3/go.mod h1:vIRUT25mLQaSh4C8H/lIsKppQz/Gdb8Pu/tNwpi52ts=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8 h1:4KCscI9qYWMGTuz6BpJtbUSRzcBrUSSE0ENMJbNSrFs=
github.com/go-text/typesetting-utils v0.0.0-20250618110550-c820a94c77b8/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6 h1:IIVxLyDUYErC950b8kecjoqDet8P5S4lcVRUOM6rdkU=
github.com/howeyc/crc16 v0.0.0-20171223171357-2b2a61e366a6/go.mod h1:JslaLRrzGsOKJgFEPBP65Whn+rdwDQSk0I0MCRFe2Zw=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/loov/hrtime v1.0.4 h1:K0wPQBsd9mWer2Sx8zIfpyAlF4ckZovtkEMUR/l9wpU=
github.com/loov/hrtime v1.0.4/go.mod h1:VbIwDNS2gYTRoo0RjQFdqdDlBjJLXrkDIOgoA7Jvupk=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/romychs/z80go v1.0.0 h1:umlM6U/JPt/qSAUhxu51wSK5a98gJZwhTu34Xj+5Ml8=
github.com/romychs/z80go v1.0.0/go.mod h1:bah4U8xv8qWfRJsrioP643bri+LDzKyC03E718YS9wI=
github.com/romychs/z80go v1.0.1 h1:rBYxvvdWHGsitZMXTheZGk6lB9VIepUUVWMxPVWimPY=
github.com/romychs/z80go v1.0.1/go.mod h1:bah4U8xv8qWfRJsrioP643bri+LDzKyC03E718YS9wI=
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sirupsen/logrus v1.9.4 h1:TsZE7l11zFCLZnZ+teH4Umoq5BhEIfIzfRDZ1Uzql2w=
github.com/sirupsen/logrus v1.9.4/go.mod h1:ftWc9WdOfJ0a92nsE2jF5u5ZwH8Bv2zdeOC42RjbV2g=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -1,12 +0,0 @@
Copyright (c) 2017, Paessler AG <support@paessler.com>
All rights reserved.
Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@ -1,150 +0,0 @@
package gval
import (
"context"
"testing"
)
func BenchmarkGval(bench *testing.B) {
benchmarks := []evaluationTest{
{
// Serves as a "water test" to give an idea of the general overhead
name: "const",
expression: "1",
},
{
name: "single parameter",
expression: "requests_made",
parameter: map[string]interface{}{
"requests_made": 99.0,
},
},
{
name: "parameter",
expression: "requests_made > requests_succeeded",
parameter: map[string]interface{}{
"requests_made": 99.0,
"requests_succeeded": 90.0,
},
},
{
// The most common use case, a single variable, modified slightly, compared to a constant.
// This is the "expected" use case.
name: "common",
expression: "(requests_made * requests_succeeded / 100) >= 90",
parameter: map[string]interface{}{
"requests_made": 99.0,
"requests_succeeded": 90.0,
},
},
{
// All major possibilities in one expression.
name: "complex",
expression: `2 > 1 &&
"something" != "nothing" ||
date("2014-01-20") < date("Wed Jul 8 23:07:35 MDT 2015") &&
object["Variable name with spaces"] <= array[0] &&
modifierTest + 1000 / 2 > (80 * 100 % 2)`,
parameter: map[string]interface{}{
"object": map[string]interface{}{"Variable name with spaces": 10.},
"array": []interface{}{0.},
"modifierTest": 7.3,
},
},
{
// no variables, no modifiers
name: "literal",
expression: "(2) > (1)",
},
{
name: "modifier",
expression: "(2) + (2) == (4)",
},
{
// Benchmarks uncompiled parameter regex operators, which are the most expensive of the lot.
// Note that regex compilation times are unpredictable and wily things. The regex engine has a lot of edge cases
// and possible performance pitfalls. This test doesn't aim to be comprehensive against all possible regex scenarios,
// it is primarily concerned with tracking how much longer it takes to compile a regex at evaluation-time than during parse-time.
name: "regex",
expression: "(foo !~ bar) && (foo + bar =~ oba)",
parameter: map[string]interface{}{
"foo": "foo",
"bar": "bar",
"baz": "baz",
"oba": ".*oba.*",
},
},
{
// Benchmarks pre-compilable regex patterns. Meant to serve as a sanity check that constant strings used as regex patterns
// are actually being precompiled.
// Also demonstrates that (generally) compiling a regex at evaluation-time takes an order of magnitude more time than pre-compiling.
name: "constant regex",
expression: `(foo !~ "[bB]az") && (bar =~ "[bB]ar")`,
parameter: map[string]interface{}{
"foo": "foo",
"bar": "bar",
"baz": "baz",
"oba": ".*oba.*",
},
},
{
name: "accessors",
expression: "foo.Int",
parameter: fooFailureParameters,
},
{
name: "accessors method",
expression: "foo.Func()",
parameter: fooFailureParameters,
},
{
name: "accessors method parameter",
expression: `foo.FuncArgStr("bonk")`,
parameter: fooFailureParameters,
},
{
name: "nested accessors",
expression: `foo.Nested.Funk`,
parameter: fooFailureParameters,
},
{
name: "decimal arithmetic",
expression: "(requests_made * requests_succeeded / 100)",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"requests_made": 99.0,
"requests_succeeded": 90.0,
},
},
{
name: "decimal logic",
expression: "(requests_made * requests_succeeded / 100) >= 90",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"requests_made": 99.0,
"requests_succeeded": 90.0,
},
},
}
for _, benchmark := range benchmarks {
eval, err := Full().NewEvaluable(benchmark.expression)
if err != nil {
bench.Fatal(err)
}
_, err = eval(context.Background(), benchmark.parameter)
if err != nil {
bench.Fatal(err)
}
bench.Run(benchmark.name+"_evaluation", func(bench *testing.B) {
for i := 0; i < bench.N; i++ {
eval(context.Background(), benchmark.parameter)
}
})
bench.Run(benchmark.name+"_parsing", func(bench *testing.B) {
for i := 0; i < bench.N; i++ {
Full().NewEvaluable(benchmark.expression)
}
})
}
}

View File

@ -1,366 +0,0 @@
package gval
import (
"context"
"fmt"
"reflect"
"regexp"
"strconv"
)
// Selector allows for custom variable selection from structs
//
// Return value is again handled with variable() until end of the given path
type Selector interface {
SelectGVal(c context.Context, key string) (interface{}, error)
}
// Evaluable evaluates given parameter
type Evaluable func(c context.Context, parameter interface{}) (interface{}, error)
// EvalInt evaluates given parameter to an int
func (e Evaluable) EvalInt(c context.Context, parameter interface{}) (int, error) {
v, err := e(c, parameter)
if err != nil {
return 0, err
}
f, ok := convertToUint(v)
if !ok {
return 0, fmt.Errorf("expected number but got %v (%T)", v, v)
}
return int(f), nil
}
// EvalUint evaluates given parameter to a float64
func (e Evaluable) EvalUint(c context.Context, parameter interface{}) (uint, error) {
v, err := e(c, parameter)
if err != nil {
return 0, err
}
f, ok := convertToUint(v)
if !ok {
return 0, fmt.Errorf("expected number but got %v (%T)", v, v)
}
return f, nil
}
// EvalBool evaluates given parameter to a bool
func (e Evaluable) EvalBool(c context.Context, parameter interface{}) (bool, error) {
v, err := e(c, parameter)
if err != nil {
return false, err
}
b, ok := convertToBool(v)
if !ok {
return false, fmt.Errorf("expected bool but got %v (%T)", v, v)
}
return b, nil
}
// EvalString evaluates given parameter to a string
func (e Evaluable) EvalString(c context.Context, parameter interface{}) (string, error) {
o, err := e(c, parameter)
if err != nil {
return "", err
}
if s, ok := o.(string); ok {
return s, nil
}
return fmt.Sprintf("%v", o), nil
}
// Const Evaluable represents given constant
func (*Parser) Const(value interface{}) Evaluable {
return constant(value)
}
//go:noinline
func constant(value interface{}) Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
return value, nil
}
}
// Var Evaluable represents value at given path.
// It supports with default language VariableSelector:
//
// map[interface{}]interface{},
// map[string]interface{} and
// []interface{} and via reflect
// struct fields,
// struct methods,
// slices and
// map with int or string key.
func (p *Parser) Var(path ...Evaluable) Evaluable {
if p.selector == nil {
return variable(path)
}
return p.selector(path)
}
// Evaluables is a slice of Evaluable.
type Evaluables []Evaluable
// EvalStrings evaluates given parameter to a string slice
func (evs Evaluables) EvalStrings(c context.Context, parameter interface{}) ([]string, error) {
strs := make([]string, len(evs))
for i, p := range evs {
k, err := p.EvalString(c, parameter)
if err != nil {
return nil, err
}
strs[i] = k
}
return strs, nil
}
func variable(path Evaluables) Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
v2 := v
for _, p := range path {
k, err := p.EvalString(c, v)
if err != nil {
return nil, err
}
switch o := v2.(type) {
case Selector:
v2, err = o.SelectGVal(c, k)
if err != nil {
return nil, fmt.Errorf("failed to select '%s' on %T: %w", k, o, err)
}
continue
case map[interface{}]interface{}:
v2 = o[k]
continue
case map[string]interface{}:
v2 = o[k]
continue
case []interface{}:
if i, err := strconv.Atoi(k); err == nil && i >= 0 && len(o) > i {
v2 = o[i]
continue
}
default:
var ok bool
v2, ok = reflectSelect(k, o)
if !ok {
return nil, fmt.Errorf("unknown parameter '%s' on %T", k, o)
}
}
}
return v2, nil
}
}
func reflectSelect(key string, value interface{}) (selection interface{}, ok bool) {
vv := reflect.ValueOf(value)
vvElem := resolvePotentialPointer(vv)
switch vvElem.Kind() {
case reflect.Map:
mapKey, ok := reflectConvertTo(vv.Type().Key().Kind(), key)
if !ok {
return nil, false
}
vvElem = vv.MapIndex(reflect.ValueOf(mapKey))
vvElem = resolvePotentialPointer(vvElem)
if vvElem.IsValid() {
return vvElem.Interface(), true
}
// key didn't exist. Check if there is a bound method
method := vv.MethodByName(key)
if method.IsValid() {
return method.Interface(), true
}
case reflect.Slice:
if i, err := strconv.Atoi(key); err == nil && i >= 0 && vv.Len() > i {
vvElem = resolvePotentialPointer(vv.Index(i))
return vvElem.Interface(), true
}
// key not an int. Check if there is a bound method
method := vv.MethodByName(key)
if method.IsValid() {
return method.Interface(), true
}
case reflect.Struct:
field := vvElem.FieldByName(key)
if field.IsValid() {
return field.Interface(), true
}
method := vv.MethodByName(key)
if method.IsValid() {
return method.Interface(), true
}
}
return nil, false
}
func resolvePotentialPointer(value reflect.Value) reflect.Value {
if value.Kind() == reflect.Ptr {
return value.Elem()
}
return value
}
func reflectConvertTo(k reflect.Kind, value string) (interface{}, bool) {
switch k {
case reflect.String:
return value, true
case reflect.Int:
if i, err := strconv.Atoi(value); err == nil {
return i, true
}
}
return nil, false
}
func (*Parser) callFunc(fun function, args ...Evaluable) Evaluable {
return func(c context.Context, v interface{}) (ret interface{}, err error) {
a := make([]interface{}, len(args))
for i, arg := range args {
ai, err := arg(c, v)
if err != nil {
return nil, err
}
a[i] = ai
}
return fun(c, a...)
}
}
func (*Parser) callEvaluable(fullname string, fun Evaluable, args ...Evaluable) Evaluable {
return func(c context.Context, v interface{}) (ret interface{}, err error) {
f, err := fun(c, v)
if err != nil {
return nil, fmt.Errorf("could not call function: %w", err)
}
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("failed to execute function '%s': %s", fullname, r)
ret = nil
}
}()
ff := reflect.ValueOf(f)
if ff.Kind() != reflect.Func {
return nil, fmt.Errorf("could not call '%s' type %T", fullname, f)
}
a := make([]reflect.Value, len(args))
for i := range args {
arg, err := args[i](c, v)
if err != nil {
return nil, err
}
a[i] = reflect.ValueOf(arg)
}
rr := ff.Call(a)
r := make([]interface{}, len(rr))
for i, e := range rr {
r[i] = e.Interface()
}
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if len(r) > 0 && ff.Type().Out(len(r)-1).Implements(errorInterface) {
if r[len(r)-1] != nil {
err = r[len(r)-1].(error)
}
r = r[0 : len(r)-1]
}
switch len(r) {
case 0:
return err, nil
case 1:
return r[0], err
default:
return r, err
}
}
}
// IsConst returns if the Evaluable is a Parser.Const() value
func (e Evaluable) IsConst() bool {
pc := reflect.ValueOf(constant(nil)).Pointer()
pe := reflect.ValueOf(e).Pointer()
return pc == pe
}
func regEx(a, b Evaluable) (Evaluable, error) {
if !b.IsConst() {
return func(c context.Context, o interface{}) (interface{}, error) {
a, err := a.EvalString(c, o)
if err != nil {
return nil, err
}
b, err := b.EvalString(c, o)
if err != nil {
return nil, err
}
matched, err := regexp.MatchString(b, a)
return matched, err
}, nil
}
s, err := b.EvalString(context.TODO(), nil)
if err != nil {
return nil, err
}
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
return func(c context.Context, v interface{}) (interface{}, error) {
s, err := a.EvalString(c, v)
if err != nil {
return nil, err
}
return regex.MatchString(s), nil
}, nil
}
func notRegEx(a, b Evaluable) (Evaluable, error) {
if !b.IsConst() {
return func(c context.Context, o interface{}) (interface{}, error) {
a, err := a.EvalString(c, o)
if err != nil {
return nil, err
}
b, err := b.EvalString(c, o)
if err != nil {
return nil, err
}
matched, err := regexp.MatchString(b, a)
return !matched, err
}, nil
}
s, err := b.EvalString(context.TODO(), nil)
if err != nil {
return nil, err
}
regex, err := regexp.Compile(s)
if err != nil {
return nil, err
}
return func(c context.Context, v interface{}) (interface{}, error) {
s, err := a.EvalString(c, v)
if err != nil {
return nil, err
}
return !regex.MatchString(s), nil
}, nil
}

View File

@ -1,250 +0,0 @@
package gval
import (
"context"
"fmt"
"reflect"
"strings"
"testing"
"time"
)
func TestEvaluable_IsConst(t *testing.T) {
p := Parser{}
tests := []struct {
name string
e Evaluable
want bool
}{
{
"const",
p.Const(80.5),
true,
},
{
"var",
p.Var(),
false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.e.IsConst(); got != tt.want {
t.Errorf("Evaluable.IsConst() = %v, want %v", got, tt.want)
}
})
}
}
func TestEvaluable_EvalInt(t *testing.T) {
tests := []struct {
name string
e Evaluable
want int
wantErr bool
}{
{
"point",
constant("5.3"),
5,
false,
},
{
"number",
constant(255.),
255,
false,
},
{
"error",
constant("5.3 cm"),
0,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.e.EvalInt(context.Background(), nil)
if (err != nil) != tt.wantErr {
t.Errorf("Evaluable.EvalInt() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Evaluable.EvalInt() = %v, want %v", got, tt.want)
}
})
}
}
func TestEvaluable_EvalFloat64(t *testing.T) {
tests := []struct {
name string
e Evaluable
want float64
wantErr bool
}{
{
"point",
constant("5.3"),
5.3,
false,
},
{
"number",
constant(255.),
255,
false,
},
{
"error",
constant("5.3 cm"),
0,
true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := tt.e.EvalUint(context.Background(), nil)
if (err != nil) != tt.wantErr {
t.Errorf("Evaluable.EvalUint() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("Evaluable.EvalUint() = %v, want %v", got, tt.want)
}
})
}
}
type testSelector struct {
str string
Map map[string]interface{}
}
func (s testSelector) SelectGVal(ctx context.Context, k string) (interface{}, error) {
if k == "str" {
return s.str, nil
}
if k == "map" {
return s.Map, nil
}
if strings.HasPrefix(k, "deep") {
return s, nil
}
return nil, fmt.Errorf("unknown-key")
}
func TestEvaluable_CustomSelector(t *testing.T) {
var (
lang = Base()
tests = []struct {
name string
expr string
params interface{}
want interface{}
wantErr bool
}{
{
"unknown",
"s.Foo",
map[string]interface{}{"s": &testSelector{}},
nil,
true,
},
{
"field directly",
"s.Str",
map[string]interface{}{"s": &testSelector{str: "test-value"}},
nil,
true,
},
{
"field via selector",
"s.str",
map[string]interface{}{"s": &testSelector{str: "test-value"}},
"test-value",
false,
},
{
"flat",
"str",
&testSelector{str: "test-value"},
"test-value",
false,
},
{
"map field",
"s.map.foo",
map[string]interface{}{"s": &testSelector{Map: map[string]interface{}{"foo": "bar"}}},
"bar",
false,
},
{
"crawl to val",
"deep.deeper.deepest.str",
&testSelector{str: "foo"},
"foo",
false,
},
{
"crawl to struct",
"deep.deeper.deepest",
&testSelector{},
testSelector{},
false,
},
}
booltests = []struct {
name string
expr string
params interface{}
want interface{}
wantErr bool
}{
{
"test method",
"s.IsZero",
map[string]interface{}{"s": time.Now()},
false,
false,
},
}
)
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := lang.Evaluate(tt.expr, tt.params)
if (err != nil) != tt.wantErr {
t.Errorf("Evaluable.Evaluate() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Evaluable.Evaluate() = %v, want %v", got, tt.want)
}
})
}
for _, tt := range booltests {
t.Run(tt.name, func(t *testing.T) {
got, err := lang.Evaluate(tt.expr, tt.params)
if (err != nil) != tt.wantErr {
t.Errorf("Evaluable.Evaluate() error = %v, wantErr %v", err, tt.wantErr)
return
}
got, ok := convertToBool(got)
if !ok {
t.Errorf("Evaluable.Evaluate() error = nok, wantErr %v", tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Evaluable.Evaluate() = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,450 +0,0 @@
package gval_test
import (
"context"
"fmt"
"strings"
"time"
"github.com/PaesslerAG/gval"
"github.com/PaesslerAG/jsonpath"
)
func Example() {
vars := map[string]interface{}{"name": "World"}
value, err := gval.Evaluate(`"Hello " + name + "!"`, vars)
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// Hello World!
}
func ExampleEvaluate() {
value, err := gval.Evaluate("foo > 0", map[string]interface{}{
"foo": -1.,
})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// false
}
func ExampleEvaluate_nestedParameter() {
value, err := gval.Evaluate("foo.bar > 0", map[string]interface{}{
"foo": map[string]interface{}{"bar": -1.},
})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// false
}
func ExampleEvaluate_array() {
value, err := gval.Evaluate("foo[0]", map[string]interface{}{
"foo": []interface{}{-1.},
})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// -1
}
func ExampleEvaluate_complexAccessor() {
value, err := gval.Evaluate(`foo["b" + "a" + "r"]`, map[string]interface{}{
"foo": map[string]interface{}{"bar": -1.},
})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// -1
}
func ExampleEvaluate_arithmetic() {
value, err := gval.Evaluate("(requests_made * requests_succeeded / 100) >= 90",
map[string]interface{}{
"requests_made": 100,
"requests_succeeded": 80,
})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// false
}
func ExampleEvaluate_string() {
value, err := gval.Evaluate(`http_response_body == "service is ok"`,
map[string]interface{}{
"http_response_body": "service is ok",
})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// true
}
func ExampleEvaluate_float64() {
value, err := gval.Evaluate("(mem_used / total_mem) * 100",
map[string]interface{}{
"total_mem": 1024,
"mem_used": 512,
})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// 50
}
func ExampleEvaluate_dateComparison() {
value, err := gval.Evaluate("date(`2014-01-02`) > date(`2014-01-01 23:59:59`)",
nil,
// define Date comparison because it is not part expression language gval
gval.InfixOperator(">", func(a, b interface{}) (interface{}, error) {
date1, ok1 := a.(time.Time)
date2, ok2 := b.(time.Time)
if ok1 && ok2 {
return date1.After(date2), nil
}
return nil, fmt.Errorf("unexpected operands types (%T) > (%T)", a, b)
}),
)
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// true
}
func ExampleEvaluable() {
eval, err := gval.Full(gval.Constant("maximum_time", 52)).
NewEvaluable("response_time <= maximum_time")
if err != nil {
fmt.Println(err)
}
for i := 50; i < 55; i++ {
value, err := eval(context.Background(), map[string]interface{}{
"response_time": i,
})
if err != nil {
fmt.Println(err)
}
fmt.Println(value)
}
// Output:
// true
// true
// true
// false
// false
}
func ExampleEvaluate_strlen() {
value, err := gval.Evaluate(`strlen("someReallyLongInputString") <= 16`,
nil,
gval.Function("strlen", func(args ...interface{}) (interface{}, error) {
length := len(args[0].(string))
return (float64)(length), nil
}))
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// false
}
func ExampleEvaluate_encoding() {
value, err := gval.Evaluate(`(7 < "47" == true ? "hello world!\n\u263a" : "good bye\n")`+" + ` more text`",
nil,
gval.Function("strlen", func(args ...interface{}) (interface{}, error) {
length := len(args[0].(string))
return (float64)(length), nil
}))
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// hello world!
// ☺ more text
}
type exampleType struct {
Hello string
}
func (e exampleType) World() string {
return "world"
}
func ExampleEvaluate_accessor() {
value, err := gval.Evaluate(`foo.Hello + foo.World()`,
map[string]interface{}{
"foo": exampleType{Hello: "hello "},
})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// hello world
}
func ExampleEvaluate_flatAccessor() {
value, err := gval.Evaluate(`Hello + World()`,
exampleType{Hello: "hello "},
)
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// hello world
}
func ExampleEvaluate_nestedAccessor() {
value, err := gval.Evaluate(`foo.Bar.Hello + foo.Bar.World()`,
map[string]interface{}{
"foo": struct{ Bar exampleType }{
Bar: exampleType{Hello: "hello "},
},
})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// hello world
}
func ExampleVariableSelector() {
value, err := gval.Evaluate(`hello.world`,
"!",
gval.VariableSelector(func(path gval.Evaluables) gval.Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
keys, err := path.EvalStrings(c, v)
if err != nil {
return nil, err
}
return fmt.Sprintf("%s%s", strings.Join(keys, " "), v), nil
}
}),
)
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// hello world!
}
func ExampleEvaluable_EvalInt() {
eval, err := gval.Full().NewEvaluable("1 + x")
if err != nil {
fmt.Println(err)
return
}
value, err := eval.EvalInt(context.Background(), map[string]interface{}{"x": 5})
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// 6
}
func ExampleEvaluable_EvalBool() {
eval, err := gval.Full().NewEvaluable("1 == x")
if err != nil {
fmt.Println(err)
return
}
value, err := eval.EvalBool(context.Background(), map[string]interface{}{"x": 1})
if err != nil {
fmt.Println(err)
}
if value {
fmt.Print("yeah")
}
// Output:
// yeah
}
func ExampleEvaluate_jsonpath() {
value, err := gval.Evaluate(`$["response-time"]`,
map[string]interface{}{
"response-time": 100,
},
jsonpath.Language(),
)
if err != nil {
fmt.Println(err)
}
fmt.Print(value)
// Output:
// 100
}
func ExampleLanguage() {
lang := gval.NewLanguage(gval.JSON(), gval.Arithmetic(),
//pipe operator
gval.PostfixOperator("|", func(c context.Context, p *gval.Parser, pre gval.Evaluable) (gval.Evaluable, error) {
post, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
return func(c context.Context, v interface{}) (interface{}, error) {
v, err := pre(c, v)
if err != nil {
return nil, err
}
return post(c, v)
}, nil
}))
eval, err := lang.NewEvaluable(`{"foobar": 50} | foobar + 100`)
if err != nil {
fmt.Println(err)
}
value, err := eval(context.Background(), nil)
if err != nil {
fmt.Println(err)
}
fmt.Println(value)
// Output:
// 150
}
type exampleCustomSelector struct{ hidden string }
var _ gval.Selector = &exampleCustomSelector{}
func (s *exampleCustomSelector) SelectGVal(ctx context.Context, k string) (interface{}, error) {
if k == "hidden" {
return s.hidden, nil
}
return nil, nil
}
func ExampleSelector() {
lang := gval.Base()
value, err := lang.Evaluate(
"myStruct.hidden",
map[string]interface{}{"myStruct": &exampleCustomSelector{hidden: "hello world"}},
)
if err != nil {
fmt.Println(err)
}
fmt.Println(value)
// Output:
// hello world
}
func parseSub(ctx context.Context, p *gval.Parser) (gval.Evaluable, error) {
return p.ParseSublanguage(ctx, subLang)
}
var (
superLang = gval.NewLanguage(
gval.PrefixExtension('$', parseSub),
)
subLang = gval.NewLanguage(
gval.Init(func(ctx context.Context, p *gval.Parser) (gval.Evaluable, error) { return p.Const("hello world"), nil }),
)
)
func ExampleParser_ParseSublanguage() {
value, err := superLang.Evaluate("$", nil)
if err != nil {
fmt.Println(err)
}
fmt.Println(value)
// Output:
// hello world
}

View File

@ -1,128 +0,0 @@
package gval
import (
"context"
"fmt"
"reflect"
)
type function func(ctx context.Context, arguments ...interface{}) (interface{}, error)
func toFunc(f interface{}) function {
if f, ok := f.(func(arguments ...interface{}) (interface{}, error)); ok {
return function(func(ctx context.Context, arguments ...interface{}) (interface{}, error) {
var v interface{}
errCh := make(chan error, 1)
go func() {
defer func() {
if recovered := recover(); recovered != nil {
errCh <- fmt.Errorf("%v", recovered)
}
}()
result, err := f(arguments...)
v = result
errCh <- err
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-errCh:
close(errCh)
return v, err
}
})
}
if f, ok := f.(func(ctx context.Context, arguments ...interface{}) (interface{}, error)); ok {
return function(f)
}
fun := reflect.ValueOf(f)
t := fun.Type()
return func(ctx context.Context, args ...interface{}) (interface{}, error) {
var v interface{}
errCh := make(chan error, 1)
go func() {
defer func() {
if recovered := recover(); recovered != nil {
errCh <- fmt.Errorf("%v", recovered)
}
}()
in, err := createCallArguments(ctx, t, args)
if err != nil {
errCh <- err
return
}
out := fun.Call(in)
r := make([]interface{}, len(out))
for i, e := range out {
r[i] = e.Interface()
}
err = nil
errorInterface := reflect.TypeOf((*error)(nil)).Elem()
if len(r) > 0 && t.Out(len(r)-1).Implements(errorInterface) {
if r[len(r)-1] != nil {
err = r[len(r)-1].(error)
}
r = r[0 : len(r)-1]
}
switch len(r) {
case 0:
v = nil
case 1:
v = r[0]
default:
v = r
}
errCh <- err
}()
select {
case <-ctx.Done():
return nil, ctx.Err()
case err := <-errCh:
close(errCh)
return v, err
}
}
}
func createCallArguments(ctx context.Context, t reflect.Type, args []interface{}) ([]reflect.Value, error) {
variadic := t.IsVariadic()
numIn := t.NumIn()
// if first argument is a context, use the given execution context
if numIn > 0 {
thisFun := reflect.ValueOf(createCallArguments)
thisT := thisFun.Type()
if t.In(0) == thisT.In(0) {
args = append([]interface{}{ctx}, args...)
}
}
if (!variadic && len(args) != numIn) || (variadic && len(args) < numIn-1) {
return nil, fmt.Errorf("invalid number of parameters")
}
in := make([]reflect.Value, len(args))
var inType reflect.Type
for i, arg := range args {
if !variadic || i < numIn-1 {
inType = t.In(i)
} else if i == numIn-1 {
inType = t.In(numIn - 1).Elem()
}
argVal := reflect.ValueOf(arg)
if arg == nil {
argVal = reflect.Zero(reflect.TypeOf((*interface{})(nil)).Elem())
} else if !argVal.Type().AssignableTo(inType) {
return nil, fmt.Errorf("expected type %s for parameter %d but got %T",
inType.String(), i, arg)
}
in[i] = argVal
}
return in, nil
}

View File

@ -1,162 +0,0 @@
package gval
import (
"context"
"fmt"
"reflect"
"testing"
"time"
)
func Test_toFunc(t *testing.T) {
myError := fmt.Errorf("my error")
tests := []struct {
name string
function interface{}
arguments []interface{}
want interface{}
wantErr error
wantAnyErr bool
}{
{
name: "empty",
function: func() {},
},
{
name: "one arg",
function: func(a interface{}) {
if a != true {
panic("fail")
}
},
arguments: []interface{}{true},
},
{
name: "three args",
function: func(a, b, c interface{}) {
if a != 1 || b != 2 || c != 3 {
panic("fail")
}
},
arguments: []interface{}{1, 2, 3},
},
{
name: "input types",
function: func(a int, b string, c bool) {
if a != 1 || b != "2" || !c {
panic("fail")
}
},
arguments: []interface{}{1, "2", true},
},
{
name: "wronge input type int",
function: func(a int, b string, c bool) {},
arguments: []interface{}{"1", "2", true},
wantAnyErr: true,
},
{
name: "wronge input type string",
function: func(a int, b string, c bool) {},
arguments: []interface{}{1, 2, true},
wantAnyErr: true,
},
{
name: "wronge input type bool",
function: func(a int, b string, c bool) {},
arguments: []interface{}{1, "2", "true"},
wantAnyErr: true,
},
{
name: "wronge input number",
function: func(a int, b string, c bool) {},
arguments: []interface{}{1, "2"},
wantAnyErr: true,
},
{
name: "one return",
function: func() bool {
return true
},
want: true,
},
{
name: "three returns",
function: func() (bool, string, int) {
return true, "2", 3
},
want: []interface{}{true, "2", 3},
},
{
name: "error",
function: func() error {
return myError
},
wantErr: myError,
},
{
name: "none error",
function: func() error {
return nil
},
},
{
name: "one return with error",
function: func() (bool, error) {
return false, myError
},
want: false,
wantErr: myError,
},
{
name: "three returns with error",
function: func() (bool, string, int, error) {
return false, "", 0, myError
},
want: []interface{}{false, "", 0},
wantErr: myError,
},
{
name: "context not expiring",
function: func(ctx context.Context) error {
return nil
},
},
{
name: "context expires",
function: func(ctx context.Context) error {
time.Sleep(20 * time.Millisecond)
return nil
},
wantErr: context.DeadlineExceeded,
},
{
name: "nil arg",
function: func(a interface{}) bool {
return a == nil
},
arguments: []interface{}{nil},
want: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond)
got, err := toFunc(tt.function)(ctx, tt.arguments...)
cancel()
if tt.wantAnyErr {
if err != nil {
return
}
t.Fatalf("toFunc()(args...) = error(nil), but wantAnyErr")
}
if err != tt.wantErr {
t.Fatalf("toFunc()(args...) = error(%v), wantErr (%v)", err, tt.wantErr)
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("toFunc()(args...) = %v, want %v", got, tt.want)
}
})
}
}

View File

@ -1,334 +0,0 @@
// Package gval provides a generic expression language.
// All functions, infix and prefix operators can be replaced by composing languages into a new one.
//
// The package contains concrete expression languages for common application in text, arithmetic, decimal arithmetic, propositional logic and so on.
// They can be used as basis for a custom expression language or to evaluate expressions directly.
package gval
import (
"context"
"fmt"
"reflect"
"text/scanner"
"time"
"github.com/shopspring/decimal"
)
// Evaluate given parameter with given expression in gval full language
func Evaluate(expression string, parameter interface{}, opts ...Language) (interface{}, error) {
return EvaluateWithContext(context.Background(), expression, parameter, opts...)
}
// Evaluate given parameter with given expression in gval full language using a context
func EvaluateWithContext(c context.Context, expression string, parameter interface{}, opts ...Language) (interface{}, error) {
l := full
if len(opts) > 0 {
l = NewLanguage(append([]Language{l}, opts...)...)
}
return l.EvaluateWithContext(c, expression, parameter)
}
// Full is the union of Arithmetic, Bitmask, Text, PropositionalLogic, TernaryOperator, and Json
//
// Operator in: a in b is true iff value a is an element of array b
// Operator ??: a ?? b returns a if a is not false or nil, otherwise n
//
// Function Date: Date(a) parses string a. a must match RFC3339, ISO8601, ruby date, or unix date
func Full(extensions ...Language) Language {
if len(extensions) == 0 {
return full
}
return NewLanguage(append([]Language{full}, extensions...)...)
}
// TernaryOperator contains following Operator
//
// ?: a ? b : c returns b if bool a is true, otherwise b
func TernaryOperator() Language {
return ternaryOperator
}
// Arithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-)
// and numerical order (<=,<,>,>=)
//
// Arithmetic operators expect float64 operands.
// Called with unfitting input, they try to convert the input to float64.
// They can parse strings and convert any type of int or float.
func Arithmetic() Language {
return arithmetic
}
// DecimalArithmetic contains base, plus(+), minus(-), divide(/), power(**), negative(-)
// and numerical order (<=,<,>,>=)
//
// DecimalArithmetic operators expect decimal.Decimal operands (github.com/shopspring/decimal)
// and are used to calculate money/decimal rather than floating point calculations.
// Called with unfitting input, they try to convert the input to decimal.Decimal.
// They can parse strings and convert any type of int or float.
func DecimalArithmetic() Language {
return decimalArithmetic
}
// Bitmask contains base, bitwise and(&), bitwise or(|) and bitwise not(^).
//
// Bitmask operators expect float64 operands.
// Called with unfitting input they try to convert the input to float64.
// They can parse strings and convert any type of int or float.
func Bitmask() Language {
return bitmask
}
// Text contains base, lexical order on strings (<=,<,>,>=),
// regex match (=~) and regex not match (!~)
func Text() Language {
return text
}
// PropositionalLogic contains base, not(!), and (&&), or (||) and Base.
//
// Propositional operator expect bool operands.
// Called with unfitting input they try to convert the input to bool.
// Numbers other than 0 and the strings "TRUE" and "true" are interpreted as true.
// 0 and the strings "FALSE" and "false" are interpreted as false.
func PropositionalLogic() Language {
return propositionalLogic
}
// JSON contains json objects ({string:expression,...})
// and json arrays ([expression, ...])
func JSON() Language {
return ljson
}
// Parentheses contains support for parentheses.
func Parentheses() Language {
return parentheses
}
// Ident contains support for variables and functions.
func Ident() Language {
return ident
}
// Base contains equal (==) and not equal (!=), perentheses and general support for variables, constants and functions
// It contains true, false, (floating point) number, string ("" or “) and char (”) constants
func Base() Language {
return base
}
var full = NewLanguage(arithmetic, bitmask, text, propositionalLogic, ljson,
InfixOperator("in", inArray),
InfixShortCircuit("??", func(a interface{}) (interface{}, bool) {
v := reflect.ValueOf(a)
return a, a != nil && !v.IsZero()
}),
InfixOperator("??", func(a, b interface{}) (interface{}, error) {
if v := reflect.ValueOf(a); a == nil || v.IsZero() {
return b, nil
}
return a, nil
}),
ternaryOperator,
Function("date", func(arguments ...interface{}) (interface{}, error) {
if len(arguments) != 1 {
return nil, fmt.Errorf("date() expects exactly one string argument")
}
s, ok := arguments[0].(string)
if !ok {
return nil, fmt.Errorf("date() expects exactly one string argument")
}
for _, format := range [...]string{
time.ANSIC,
time.UnixDate,
time.RubyDate,
time.Kitchen,
time.RFC3339,
time.RFC3339Nano,
"2006-01-02", // RFC 3339
"2006-01-02 15:04", // RFC 3339 with minutes
"2006-01-02 15:04:05", // RFC 3339 with seconds
"2006-01-02 15:04:05-07:00", // RFC 3339 with seconds and timezone
"2006-01-02T15Z0700", // ISO8601 with hour
"2006-01-02T15:04Z0700", // ISO8601 with minutes
"2006-01-02T15:04:05Z0700", // ISO8601 with seconds
"2006-01-02T15:04:05.999999999Z0700", // ISO8601 with nanoseconds
} {
ret, err := time.ParseInLocation(format, s, time.Local)
if err == nil {
return ret, nil
}
}
return nil, fmt.Errorf("date() could not parse %s", s)
}),
)
var ternaryOperator = PostfixOperator("?", parseIf)
var ljson = NewLanguage(
PrefixExtension('[', parseJSONArray),
PrefixExtension('{', parseJSONObject),
)
var arithmetic = NewLanguage(
InfixNumberOperator("+", func(a, b uint) (interface{}, error) { return a + b, nil }),
InfixNumberOperator("-", func(a, b uint) (interface{}, error) { return a - b, nil }),
InfixNumberOperator("*", func(a, b uint) (interface{}, error) { return a * b, nil }),
InfixNumberOperator("/", func(a, b uint) (interface{}, error) { return a / b, nil }),
InfixNumberOperator("%", func(a, b uint) (interface{}, error) { return a % b, nil }),
InfixNumberOperator(">", func(a, b uint) (interface{}, error) { return a > b, nil }),
InfixNumberOperator(">=", func(a, b uint) (interface{}, error) { return a >= b, nil }),
InfixNumberOperator("<", func(a, b uint) (interface{}, error) { return a < b, nil }),
InfixNumberOperator("<=", func(a, b uint) (interface{}, error) { return a <= b, nil }),
InfixNumberOperator("==", func(a, b uint) (interface{}, error) { return a == b, nil }),
InfixNumberOperator("!=", func(a, b uint) (interface{}, error) { return a != b, nil }),
base,
)
var decimalArithmetic = NewLanguage(
InfixDecimalOperator("+", func(a, b decimal.Decimal) (interface{}, error) { return a.Add(b), nil }),
InfixDecimalOperator("-", func(a, b decimal.Decimal) (interface{}, error) { return a.Sub(b), nil }),
InfixDecimalOperator("*", func(a, b decimal.Decimal) (interface{}, error) { return a.Mul(b), nil }),
InfixDecimalOperator("/", func(a, b decimal.Decimal) (interface{}, error) { return a.Div(b), nil }),
InfixDecimalOperator("%", func(a, b decimal.Decimal) (interface{}, error) { return a.Mod(b), nil }),
InfixDecimalOperator("**", func(a, b decimal.Decimal) (interface{}, error) { return a.Pow(b), nil }),
InfixDecimalOperator(">", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThan(b), nil }),
InfixDecimalOperator(">=", func(a, b decimal.Decimal) (interface{}, error) { return a.GreaterThanOrEqual(b), nil }),
InfixDecimalOperator("<", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThan(b), nil }),
InfixDecimalOperator("<=", func(a, b decimal.Decimal) (interface{}, error) { return a.LessThanOrEqual(b), nil }),
InfixDecimalOperator("==", func(a, b decimal.Decimal) (interface{}, error) { return a.Equal(b), nil }),
InfixDecimalOperator("!=", func(a, b decimal.Decimal) (interface{}, error) { return !a.Equal(b), nil }),
base,
//Base is before these overrides so that the Base options are overridden
PrefixExtension(scanner.Int, parseDecimal),
PrefixExtension(scanner.Float, parseDecimal),
PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToUint(v)
if !ok {
return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v)
}
return -i, nil
}),
)
var bitmask = NewLanguage(
InfixNumberOperator("^", func(a, b uint) (interface{}, error) { return uint(int64(a) ^ int64(b)), nil }),
InfixNumberOperator("&", func(a, b uint) (interface{}, error) { return uint(int64(a) & int64(b)), nil }),
InfixNumberOperator("|", func(a, b uint) (interface{}, error) { return uint(int64(a) | int64(b)), nil }),
InfixNumberOperator("<<", func(a, b uint) (interface{}, error) { return uint(int64(a) << uint64(b)), nil }),
InfixNumberOperator(">>", func(a, b uint) (interface{}, error) { return uint(int64(a) >> uint64(b)), nil }),
PrefixOperator("~", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToUint(v)
if !ok {
return nil, fmt.Errorf("unexpected %T expected number", v)
}
return float64(^int64(i)), nil
}),
)
var text = NewLanguage(
InfixTextOperator("+", func(a, b string) (interface{}, error) { return fmt.Sprintf("%v%v", a, b), nil }),
InfixTextOperator("<", func(a, b string) (interface{}, error) { return a < b, nil }),
InfixTextOperator("<=", func(a, b string) (interface{}, error) { return a <= b, nil }),
InfixTextOperator(">", func(a, b string) (interface{}, error) { return a > b, nil }),
InfixTextOperator(">=", func(a, b string) (interface{}, error) { return a >= b, nil }),
InfixEvalOperator("=~", regEx),
InfixEvalOperator("!~", notRegEx),
base,
)
var propositionalLogic = NewLanguage(
PrefixOperator("!", func(c context.Context, v interface{}) (interface{}, error) {
b, ok := convertToBool(v)
if !ok {
return nil, fmt.Errorf("unexpected %T expected bool", v)
}
return !b, nil
}),
InfixShortCircuit("&&", func(a interface{}) (interface{}, bool) { return false, a == false }),
InfixBoolOperator("&&", func(a, b bool) (interface{}, error) { return a && b, nil }),
InfixShortCircuit("||", func(a interface{}) (interface{}, bool) { return true, a == true }),
InfixBoolOperator("||", func(a, b bool) (interface{}, error) { return a || b, nil }),
InfixBoolOperator("==", func(a, b bool) (interface{}, error) { return a == b, nil }),
InfixBoolOperator("!=", func(a, b bool) (interface{}, error) { return a != b, nil }),
base,
)
var parentheses = NewLanguage(
PrefixExtension('(', parseParentheses),
)
var ident = NewLanguage(
PrefixMetaPrefix(scanner.Ident, parseIdent),
)
var base = NewLanguage(
PrefixExtension(scanner.Int, parseNumber),
PrefixExtension(scanner.Float, parseNumber),
PrefixOperator("-", func(c context.Context, v interface{}) (interface{}, error) {
i, ok := convertToUint(v)
if !ok {
return nil, fmt.Errorf("unexpected %v(%T) expected number", v, v)
}
return -i, nil
}),
PrefixExtension(scanner.String, parseString),
PrefixExtension(scanner.Char, parseString),
PrefixExtension(scanner.RawString, parseString),
Constant("true", true),
Constant("false", false),
InfixOperator("==", func(a, b interface{}) (interface{}, error) { return reflect.DeepEqual(a, b), nil }),
InfixOperator("!=", func(a, b interface{}) (interface{}, error) { return !reflect.DeepEqual(a, b), nil }),
parentheses,
Precedence("??", 0),
Precedence("||", 20),
Precedence("&&", 21),
Precedence("==", 40),
Precedence("!=", 40),
Precedence(">", 40),
Precedence(">=", 40),
Precedence("<", 40),
Precedence("<=", 40),
Precedence("=~", 40),
Precedence("!~", 40),
Precedence("in", 40),
Precedence("^", 60),
Precedence("&", 60),
Precedence("|", 60),
Precedence("<<", 90),
Precedence(">>", 90),
Precedence("+", 120),
Precedence("-", 120),
Precedence("*", 150),
Precedence("/", 150),
Precedence("%", 150),
Precedence("**", 200),
ident,
)

View File

@ -1,396 +0,0 @@
package gval
/*
Tests to make sure evaluation fails in the expected ways.
*/
import (
"errors"
"fmt"
"testing"
)
func TestModifierTyping(test *testing.T) {
var (
invalidOperator = "invalid operation"
unknownParameter = "unknown parameter"
invalidRegex = "error parsing regex"
tooFewArguments = "reflect: Call with too few input arguments"
tooManyArguments = "reflect: Call with too many input arguments"
mismatchedParameters = "reflect: Call using"
custom = "test error"
)
evaluationTests := []evaluationTest{
//ModifierTyping
{
name: "PLUS literal number to literal bool",
expression: "1 + true",
want: "1true", // + on string is defined
},
{
name: "PLUS number to bool",
expression: "number + bool",
want: "1true", // + on string is defined
},
{
name: "MINUS number to bool",
expression: "number - bool",
wantErr: invalidOperator,
},
{
name: "MINUS number to bool",
expression: "number - bool",
wantErr: invalidOperator,
},
{
name: "MULTIPLY number to bool",
expression: "number * bool",
wantErr: invalidOperator,
},
{
name: "DIVIDE number to bool",
expression: "number / bool",
wantErr: invalidOperator,
},
{
name: "EXPONENT number to bool",
expression: "number ** bool",
wantErr: invalidOperator,
},
{
name: "MODULUS number to bool",
expression: "number % bool",
wantErr: invalidOperator,
},
{
name: "XOR number to bool",
expression: "number % bool",
wantErr: invalidOperator,
},
{
name: "BITWISE_OR number to bool",
expression: "number | bool",
wantErr: invalidOperator,
},
{
name: "BITWISE_AND number to bool",
expression: "number & bool",
wantErr: invalidOperator,
},
{
name: "BITWISE_XOR number to bool",
expression: "number ^ bool",
wantErr: invalidOperator,
},
{
name: "BITWISE_LSHIFT number to bool",
expression: "number << bool",
wantErr: invalidOperator,
},
{
name: "BITWISE_RSHIFT number to bool",
expression: "number >> bool",
wantErr: invalidOperator,
},
//LogicalOperatorTyping
{
name: "AND number to number",
expression: "number && number",
want: true, // number != 0 is true
},
{
name: "OR number to number",
expression: "number || number",
want: true, // number != 0 is true
},
{
name: "AND string to string",
expression: "string && string",
wantErr: invalidOperator,
},
{
name: "OR string to string",
expression: "string || string",
wantErr: invalidOperator,
},
{
name: "AND number to string",
expression: "number && string",
wantErr: invalidOperator,
},
{
name: "OR number to string",
expression: "number || string",
wantErr: invalidOperator,
},
{
name: "AND bool to string",
expression: "bool && string",
wantErr: invalidOperator,
},
{
name: "OR string to bool",
expression: "string || bool",
wantErr: invalidOperator,
},
//ComparatorTyping
{
name: "GT literal bool to literal bool",
expression: "true > true",
want: false, //lexical order on "true"
},
{
name: "GT bool to bool",
expression: "bool > bool",
want: false, //lexical order on "true"
},
{
name: "GTE bool to bool",
expression: "bool >= bool",
want: true, //lexical order on "true"
},
{
name: "LT bool to bool",
expression: "bool < bool",
want: false, //lexical order on "true"
},
{
name: "LTE bool to bool",
expression: "bool <= bool",
want: true, //lexical order on "true"
},
{
name: "GT number to string",
expression: "number > string",
want: false, //lexical order "1" < "foo"
},
{
name: "GTE number to string",
expression: "number >= string",
want: false, //lexical order "1" < "foo"
},
{
name: "LT number to string",
expression: "number < string",
want: true, //lexical order "1" < "foo"
},
{
name: "REQ number to string",
expression: "number =~ string",
want: false,
},
{
name: "REQ number to bool",
expression: "number =~ bool",
want: false,
},
{
name: "REQ bool to number",
expression: "bool =~ number",
want: false,
},
{
name: "REQ bool to string",
expression: "bool =~ string",
want: false,
},
{
name: "NREQ number to string",
expression: "number !~ string",
want: true,
},
{
name: "NREQ number to bool",
expression: "number !~ bool",
want: true,
},
{
name: "NREQ bool to number",
expression: "bool !~ number",
want: true,
},
{
name: "NREQ bool to string",
expression: "bool !~ string",
want: true,
},
{
name: "IN non-array numeric",
expression: "1 in 2",
wantErr: "expected type []interface{} for in operator but got float64",
},
{
name: "IN non-array string",
expression: `1 in "foo"`,
wantErr: "expected type []interface{} for in operator but got string",
},
{
name: "IN non-array boolean",
expression: "1 in true",
wantErr: "expected type []interface{} for in operator but got bool",
},
//TernaryTyping
{
name: "Ternary with number",
expression: "10 ? true",
want: true, // 10 != nil && 10 != false
},
{
name: "Ternary with string",
expression: `"foo" ? true`,
want: true, // "foo" != nil && "foo" != false
},
//RegexParameterCompilation
{
name: "Regex equality runtime parsing",
expression: `"foo" =~ foo`,
parameter: map[string]interface{}{
"foo": "[foo",
},
wantErr: invalidRegex,
},
{
name: "Regex inequality runtime parsing",
expression: `"foo" !~ foo`,
parameter: map[string]interface{}{
"foo": "[foo",
},
wantErr: invalidRegex,
},
{
name: "Regex equality runtime right side evaluation",
expression: `"foo" =~ error()`,
wantErr: custom,
},
{
name: "Regex inequality runtime right side evaluation",
expression: `"foo" !~ error()`,
wantErr: custom,
},
{
name: "Regex equality runtime left side evaluation",
expression: `error() =~ "."`,
wantErr: custom,
},
{
name: "Regex inequality runtime left side evaluation",
expression: `error() !~ "."`,
wantErr: custom,
},
//FuncExecution
{
name: "Func error bubbling",
expression: "error()",
extension: Function("error", func(arguments ...interface{}) (interface{}, error) {
return nil, errors.New("Huge problems")
}),
wantErr: "Huge problems",
},
//InvalidParameterCalls
{
name: "Missing parameter field reference",
expression: "foo.NotExists",
parameter: fooFailureParameters,
wantErr: unknownParameter,
},
{
name: "Parameter method call on missing function",
expression: "foo.NotExist()",
parameter: fooFailureParameters,
wantErr: unknownParameter,
},
{
name: "Nested missing parameter field reference",
expression: "foo.Nested.NotExists",
parameter: fooFailureParameters,
wantErr: unknownParameter,
},
{
name: "Parameter method call returns error",
expression: "foo.AlwaysFail()",
parameter: fooFailureParameters,
wantErr: "function should always fail",
},
{
name: "Too few arguments to parameter call",
expression: "foo.FuncArgStr()",
parameter: fooFailureParameters,
wantErr: tooFewArguments,
},
{
name: "Too many arguments to parameter call",
expression: `foo.FuncArgStr("foo", "bar", 15)`,
parameter: fooFailureParameters,
wantErr: tooManyArguments,
},
{
name: "Mismatched parameters",
expression: "foo.FuncArgStr(5)",
parameter: fooFailureParameters,
wantErr: mismatchedParameters,
},
{
name: "Negative Array Index",
expression: "foo[-1]",
parameter: map[string]interface{}{
"foo": []int{1, 2, 3},
},
wantErr: unknownParameter,
},
{
name: "Nested slice call index out of bound",
expression: `foo.Nested.Slice[10]`,
parameter: map[string]interface{}{"foo": foo},
wantErr: unknownParameter,
},
{
name: "Nested map call missing key",
expression: `foo.Nested.Map["d"]`,
parameter: map[string]interface{}{"foo": foo},
wantErr: unknownParameter,
},
{
name: "invalid selector",
expression: "hello[world()]",
extension: NewLanguage(Base(), Function("world", func() (int, error) {
return 0, fmt.Errorf("test error")
})),
wantErr: "test error",
},
{
name: "eval `nil > 1` returns true #23",
expression: `nil > 1`,
wantErr: "invalid operation (<nil>) > (float64)",
},
{
name: "map with unknown func",
expression: `foo.MapWithFunc.NotExist()`,
parameter: map[string]interface{}{"foo": foo},
wantErr: unknownParameter,
},
{
name: "map with unknown func",
expression: `foo.SliceWithFunc.NotExist()`,
parameter: map[string]interface{}{"foo": foo},
wantErr: unknownParameter,
},
}
for i := range evaluationTests {
if evaluationTests[i].parameter == nil {
evaluationTests[i].parameter = map[string]interface{}{
"number": 1,
"string": "foo",
"bool": true,
"error": func() (int, error) {
return 0, fmt.Errorf("test error")
},
}
}
}
testEvaluate(evaluationTests, test)
}

View File

@ -1,818 +0,0 @@
package gval
import (
"context"
"fmt"
"testing"
"text/scanner"
)
func TestNoParameter(t *testing.T) {
testEvaluate(
[]evaluationTest{
{
name: "Number",
expression: "100",
want: 100.0,
},
{
name: "Single PLUS",
expression: "51 + 49",
want: 100.0,
},
{
name: "Single MINUS",
expression: "100 - 51",
want: 49.0,
},
{
name: "Single BITWISE AND",
expression: "100 & 50",
want: 32.0,
},
{
name: "Single BITWISE OR",
expression: "100 | 50",
want: 118.0,
},
{
name: "Single BITWISE XOR",
expression: "100 ^ 50",
want: 86.0,
},
{
name: "Single shift left",
expression: "2 << 1",
want: 4.0,
},
{
name: "Single shift right",
expression: "2 >> 1",
want: 1.0,
},
{
name: "Single BITWISE NOT",
expression: "~10",
want: -11.0,
},
{
name: "Single MULTIPLY",
expression: "5 * 20",
want: 100.0,
},
{
name: "Single DIVIDE",
expression: "100 / 20",
want: 5.0,
},
{
name: "Single even MODULUS",
expression: "100 % 2",
want: 0.0,
},
{
name: "Single odd MODULUS",
expression: "101 % 2",
want: 1.0,
},
{
name: "Single EXPONENT",
expression: "10 ** 2",
want: 100.0,
},
{
name: "Compound PLUS",
expression: "20 + 30 + 50",
want: 100.0,
},
{
name: "Compound BITWISE AND",
expression: "20 & 30 & 50",
want: 16.0,
},
{
name: "Mutiple operators",
expression: "20 * 5 - 49",
want: 51.0,
},
{
name: "Parenthesis usage",
expression: "100 - (5 * 10)",
want: 50.0,
},
{
name: "Nested parentheses",
expression: "50 + (5 * (15 - 5))",
want: 100.0,
},
{
name: "Nested parentheses with bitwise",
expression: "100 ^ (23 * (2 | 5))",
want: 197.0,
},
{
name: "Logical OR operation of two clauses",
expression: "(1 == 1) || (true == true)",
want: true,
},
{
name: "Logical AND operation of two clauses",
expression: "(1 == 1) && (true == true)",
want: true,
},
{
name: "Implicit boolean",
expression: "2 > 1",
want: true,
},
{
name: "Equal test minus numbers and no spaces",
expression: "-1==-1",
want: true,
},
{
name: "Compound boolean",
expression: "5 < 10 && 1 < 5",
want: true,
},
{
name: "Evaluated true && false operation (for issue #8)",
expression: "1 > 10 && 11 > 10",
want: false,
},
{
name: "Evaluated true && false operation (for issue #8)",
expression: "true == true && false == true",
want: false,
},
{
name: "Parenthesis boolean",
expression: "10 < 50 && (1 != 2 && 1 > 0)",
want: true,
},
{
name: "Comparison of string constants",
expression: `"foo" == "foo"`,
want: true,
},
{
name: "NEQ comparison of string constants",
expression: `"foo" != "bar"`,
want: true,
},
{
name: "REQ comparison of string constants",
expression: `"foobar" =~ "oba"`,
want: true,
},
{
name: "NREQ comparison of string constants",
expression: `"foo" !~ "bar"`,
want: true,
},
{
name: "Multiplicative/additive order",
expression: "5 + 10 * 2",
want: 25.0,
},
{
name: "Multiple constant multiplications",
expression: "10 * 10 * 10",
want: 1000.0,
},
{
name: "Multiple adds/multiplications",
expression: "10 * 10 * 10 + 1 * 10 * 10",
want: 1100.0,
},
{
name: "Modulus operatorPrecedence",
expression: "1 + 101 % 2 * 5",
want: 6.0,
},
{
name: "Exponent operatorPrecedence",
expression: "1 + 5 ** 3 % 2 * 5",
want: 6.0,
},
{
name: "Bit shift operatorPrecedence",
expression: "50 << 1 & 90",
want: 64.0,
},
{
name: "Bit shift operatorPrecedence",
expression: "90 & 50 << 1",
want: 64.0,
},
{
name: "Bit shift operatorPrecedence amongst non-bitwise",
expression: "90 + 50 << 1 * 5",
want: 4480.0,
},
{
name: "Order of non-commutative same-operatorPrecedence operators (additive)",
expression: "1 - 2 - 4 - 8",
want: -13.0,
},
{
name: "Order of non-commutative same-operatorPrecedence operators (multiplicative)",
expression: "1 * 4 / 2 * 8",
want: 16.0,
},
{
name: "Null coalesce operatorPrecedence",
expression: "true ?? true ? 100 + 200 : 400",
want: 300.0,
},
{
name: "Identical date equivalence",
expression: `"2014-01-02 14:12:22" == "2014-01-02 14:12:22"`,
want: true,
},
{
name: "Positive date GT",
expression: `"2014-01-02 14:12:22" > "2014-01-02 12:12:22"`,
want: true,
},
{
name: "Negative date GT",
expression: `"2014-01-02 14:12:22" > "2014-01-02 16:12:22"`,
want: false,
},
{
name: "Positive date GTE",
expression: `"2014-01-02 14:12:22" >= "2014-01-02 12:12:22"`,
want: true,
},
{
name: "Negative date GTE",
expression: `"2014-01-02 14:12:22" >= "2014-01-02 16:12:22"`,
want: false,
},
{
name: "Positive date LT",
expression: `"2014-01-02 14:12:22" < "2014-01-02 16:12:22"`,
want: true,
},
{
name: "Negative date LT",
expression: `"2014-01-02 14:12:22" < "2014-01-02 11:12:22"`,
want: false,
},
{
name: "Positive date LTE",
expression: `"2014-01-02 09:12:22" <= "2014-01-02 12:12:22"`,
want: true,
},
{
name: "Negative date LTE",
expression: `"2014-01-02 14:12:22" <= "2014-01-02 11:12:22"`,
want: false,
},
{
name: "Sign prefix comparison",
expression: "-1 < 0",
want: true,
},
{
name: "Lexicographic LT",
expression: `"ab" < "abc"`,
want: true,
},
{
name: "Lexicographic LTE",
expression: `"ab" <= "abc"`,
want: true,
},
{
name: "Lexicographic GT",
expression: `"aba" > "abc"`,
want: false,
},
{
name: "Lexicographic GTE",
expression: `"aba" >= "abc"`,
want: false,
},
{
name: "Boolean sign prefix comparison",
expression: "!true == false",
want: true,
},
{
name: "Inversion of clause",
expression: "!(10 < 0)",
want: true,
},
{
name: "Negation after modifier",
expression: "10 * -10",
want: -100.0,
},
{
name: "Ternary with single boolean",
expression: "true ? 10",
want: 10.0,
},
{
name: "Ternary nil with single boolean",
expression: "false ? 10",
want: nil,
},
{
name: "Ternary with comparator boolean",
expression: "10 > 5 ? 35.50",
want: 35.50,
},
{
name: "Ternary nil with comparator boolean",
expression: "1 > 5 ? 35.50",
want: nil,
},
{
name: "Ternary with parentheses",
expression: "(5 * (15 - 5)) > 5 ? 35.50",
want: 35.50,
},
{
name: "Ternary operatorPrecedence",
expression: "true ? 35.50 > 10",
want: true,
},
{
name: "Ternary-else",
expression: "false ? 35.50 : 50",
want: 50.0,
},
{
name: "Ternary-else inside clause",
expression: "(false ? 5 : 35.50) > 10",
want: true,
},
{
name: "Ternary-else (true-case) inside clause",
expression: "(true ? 1 : 5) < 10",
want: true,
},
{
name: "Ternary-else before comparator (negative case)",
expression: "true ? 1 : 5 > 10",
want: 1.0,
},
{
name: "Nested ternaries (#32)",
expression: "(2 == 2) ? 1 : (true ? 2 : 3)",
want: 1.0,
},
{
name: "Nested ternaries, right case (#32)",
expression: "false ? 1 : (true ? 2 : 3)",
want: 2.0,
},
{
name: "Doubly-nested ternaries (#32)",
expression: "true ? (false ? 1 : (false ? 2 : 3)) : (false ? 4 : 5)",
want: 3.0,
},
{
name: "String to string concat",
expression: `"foo" + "bar" == "foobar"`,
want: true,
},
{
name: "String to float64 concat",
expression: `"foo" + 123 == "foo123"`,
want: true,
},
{
name: "Float64 to string concat",
expression: `123 + "bar" == "123bar"`,
want: true,
},
{
name: "String to date concat",
expression: `"foo" + "02/05/1970" == "foobar"`,
want: false,
},
{
name: "String to bool concat",
expression: `"foo" + true == "footrue"`,
want: true,
},
{
name: "Bool to string concat",
expression: `true + "bar" == "truebar"`,
want: true,
},
{
name: "Null coalesce left",
expression: "1 ?? 2",
want: 1.0,
},
{
name: "Array membership literals",
expression: "1 in [1, 2, 3]",
want: true,
},
{
name: "Array membership literal with inversion",
expression: "!(1 in [1, 2, 3])",
want: false,
},
{
name: "Logical operator reordering (#30)",
expression: "(true && true) || (true && false)",
want: true,
},
{
name: "Logical operator reordering without parens (#30)",
expression: "true && true || true && false",
want: true,
},
{
name: "Logical operator reordering with multiple OR (#30)",
expression: "false || true && true || false",
want: true,
},
{
name: "Left-side multiple consecutive (should be reordered) operators",
expression: "(10 * 10 * 10) > 10",
want: true,
},
{
name: "Three-part non-paren logical op reordering (#44)",
expression: "false && true || true",
want: true,
},
{
name: "Three-part non-paren logical op reordering (#44), second one",
expression: "true || false && true",
want: true,
},
{
name: "Logical operator reordering without parens (#45)",
expression: "true && true || false && false",
want: true,
},
{
name: "Single function",
expression: "foo()",
extension: Function("foo", func(arguments ...interface{}) (interface{}, error) {
return true, nil
}),
want: true,
},
{
name: "Func with argument",
expression: "passthrough(1)",
extension: Function("passthrough", func(arguments ...interface{}) (interface{}, error) {
return arguments[0], nil
}),
want: 1.0,
},
{
name: "Func with arguments",
expression: "passthrough(1, 2)",
extension: Function("passthrough", func(arguments ...interface{}) (interface{}, error) {
return arguments[0].(float64) + arguments[1].(float64), nil
}),
want: 3.0,
},
{
name: "Nested function with operatorPrecedence",
expression: "sum(1, sum(2, 3), 2 + 2, true ? 4 : 5)",
extension: Function("sum", func(arguments ...interface{}) (interface{}, error) {
sum := 0.0
for _, v := range arguments {
sum += v.(float64)
}
return sum, nil
}),
want: 14.0,
},
{
name: "Empty function and modifier, compared",
expression: "numeric()-1 > 0",
extension: Function("numeric", func(arguments ...interface{}) (interface{}, error) {
return 2.0, nil
}),
want: true,
},
{
name: "Empty function comparator",
expression: "numeric() > 0",
extension: Function("numeric", func(arguments ...interface{}) (interface{}, error) {
return 2.0, nil
}),
want: true,
},
{
name: "Empty function logical operator",
expression: "success() && !false",
extension: Function("success", func(arguments ...interface{}) (interface{}, error) {
return true, nil
}),
want: true,
},
{
name: "Empty function ternary",
expression: "nope() ? 1 : 2.0",
extension: Function("nope", func(arguments ...interface{}) (interface{}, error) {
return false, nil
}),
want: 2.0,
},
{
name: "Empty function null coalesce",
expression: "null() ?? 2",
extension: Function("null", func(arguments ...interface{}) (interface{}, error) {
return nil, nil
}),
want: 2.0,
},
{
name: "Empty function with prefix",
expression: "-ten()",
extension: Function("ten", func(arguments ...interface{}) (interface{}, error) {
return 10.0, nil
}),
want: -10.0,
},
{
name: "Empty function as part of chain",
expression: "10 - numeric() - 2",
extension: Function("numeric", func(arguments ...interface{}) (interface{}, error) {
return 5.0, nil
}),
want: 3.0,
},
{
name: "Empty function near separator",
expression: "10 in [1, 2, 3, ten(), 8]",
extension: Function("ten", func(arguments ...interface{}) (interface{}, error) {
return 10.0, nil
}),
want: true,
},
{
name: "Enclosed empty function with modifier and comparator (#28)",
expression: "(ten() - 1) > 3",
extension: Function("ten", func(arguments ...interface{}) (interface{}, error) {
return 10.0, nil
}),
want: true,
},
{
name: "Array",
expression: `[(ten() - 1) > 3, (ten() - 1),"hey"]`,
extension: Function("ten", func(arguments ...interface{}) (interface{}, error) {
return 10.0, nil
}),
want: []interface{}{true, 9., "hey"},
},
{
name: "Object",
expression: `{1: (ten() - 1) > 3, 7 + ".X" : (ten() - 1),"hello" : "hey"}`,
extension: Function("ten", func(arguments ...interface{}) (interface{}, error) {
return 10.0, nil
}),
want: map[string]interface{}{"1": true, "7.X": 9., "hello": "hey"},
},
{
name: "Object negativ value",
expression: `{1: -1,"hello" : "hey"}`,
want: map[string]interface{}{"1": -1., "hello": "hey"},
},
{
name: "Empty Array",
expression: `[]`,
want: []interface{}{},
},
{
name: "Empty Object",
expression: `{}`,
want: map[string]interface{}{},
},
{
name: "Variadic",
expression: `sum(1,2,3,4)`,
extension: Function("sum", func(arguments ...float64) (interface{}, error) {
sum := 0.
for _, a := range arguments {
sum += a
}
return sum, nil
}),
want: 10.0,
},
{
name: "Ident Operator",
expression: `1 plus 1`,
extension: InfixNumberOperator("plus", func(a, b float64) (interface{}, error) {
return a + b, nil
}),
want: 2.0,
},
{
name: "Postfix Operator",
expression: ``,
extension: PostfixOperator("§", func(_ context.Context, _ *Parser, eval Evaluable) (Evaluable, error) {
return func(ctx context.Context, parameter interface{}) (interface{}, error) {
i, err := eval.EvalInt(ctx, parameter)
if err != nil {
return nil, err
}
return fmt.Sprintf("§%d", i), nil
}, nil
}),
want: "§4",
},
{
name: "Tabs as non-whitespace",
expression: "4\t5\t6",
extension: NewLanguage(
Init(func(ctx context.Context, p *Parser) (Evaluable, error) {
p.SetWhitespace('\n', '\r', ' ')
return p.ParseExpression(ctx)
}),
InfixNumberOperator("\t", func(a, b float64) (interface{}, error) {
return a * b, nil
}),
),
want: 120.0,
},
{
name: "Handle all other prefixes",
expression: "^foo + $bar + &baz",
extension: DefaultExtension(func(ctx context.Context, p *Parser) (Evaluable, error) {
var mul int
switch p.TokenText() {
case "^":
mul = 1
case "$":
mul = 2
case "&":
mul = 3
}
switch p.Scan() {
case scanner.Ident:
return p.Const(mul * len(p.TokenText())), nil
default:
return nil, p.Expected("length multiplier", scanner.Ident)
}
}),
want: 18.0,
},
{
name: "Embed languages",
expression: "left { 5 + 5 } right",
extension: func() Language {
step := func(ctx context.Context, p *Parser, cur Evaluable) (Evaluable, error) {
next, err := p.ParseExpression(ctx)
if err != nil {
return nil, err
}
return func(ctx context.Context, parameter interface{}) (interface{}, error) {
us, err := cur.EvalString(ctx, parameter)
if err != nil {
return nil, err
}
them, err := next.EvalString(ctx, parameter)
if err != nil {
return nil, err
}
return us + them, nil
}, nil
}
return NewLanguage(
Init(func(ctx context.Context, p *Parser) (Evaluable, error) {
p.SetWhitespace()
p.SetMode(0)
return p.ParseExpression(ctx)
}),
DefaultExtension(func(ctx context.Context, p *Parser) (Evaluable, error) {
return step(ctx, p, p.Const(p.TokenText()))
}),
PrefixExtension(scanner.EOF, func(ctx context.Context, p *Parser) (Evaluable, error) {
return p.Const(""), nil
}),
PrefixExtension('{', func(ctx context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseSublanguage(ctx, Full())
if err != nil {
return nil, err
}
switch p.Scan() {
case '}':
default:
return nil, p.Expected("embedded", '}')
}
return step(ctx, p, eval)
}),
)
}(),
want: "left 10 right",
},
{
name: "Late binding",
expression: "5 * [ 10 * { 20 / [ 10 ] } ]",
extension: func() Language {
var inner, outer Language
parseCurly := func(ctx context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseSublanguage(ctx, outer)
if err != nil {
return nil, err
}
if p.Scan() != '}' {
return nil, p.Expected("end", '}')
}
return eval, nil
}
parseSquare := func(ctx context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseSublanguage(ctx, inner)
if err != nil {
return nil, err
}
if p.Scan() != ']' {
return nil, p.Expected("end", ']')
}
return eval, nil
}
inner = Full(PrefixExtension('{', parseCurly))
outer = Full(PrefixExtension('[', parseSquare))
return outer
}(),
want: 100.0,
},
},
t,
)
}

View File

@ -1,719 +0,0 @@
package gval
import (
"context"
"fmt"
"regexp"
"strings"
"testing"
"time"
"github.com/shopspring/decimal"
)
func TestParameterized(t *testing.T) {
testEvaluate(
[]evaluationTest{
{
name: "Single parameter modified by constant",
expression: "foo + 2",
parameter: map[string]interface{}{
"foo": 2.0,
},
want: 4.0,
},
{
name: "Single parameter modified by variable",
expression: "foo * bar",
parameter: map[string]interface{}{
"foo": 5.0,
"bar": 2.0,
},
want: 10.0,
},
{
name: "Single parameter modified by variable",
expression: `foo["hey"] * bar[1]`,
parameter: map[string]interface{}{
"foo": map[string]interface{}{"hey": 5.0},
"bar": []interface{}{7., 2.0},
},
want: 10.0,
},
{
name: "Multiple multiplications of the same parameter",
expression: "foo * foo * foo",
parameter: map[string]interface{}{
"foo": 10.0,
},
want: 1000.0,
},
{
name: "Multiple additions of the same parameter",
expression: "foo + foo + foo",
parameter: map[string]interface{}{
"foo": 10.0,
},
want: 30.0,
},
{
name: "NoSpaceOperator",
expression: "true&&name",
parameter: map[string]interface{}{
"name": true,
},
want: true,
},
{
name: "Parameter name sensitivity",
expression: "foo + FoO + FOO",
parameter: map[string]interface{}{
"foo": 8.0,
"FoO": 4.0,
"FOO": 2.0,
},
want: 14.0,
},
{
name: "Sign prefix comparison against prefixed variable",
expression: "-1 < -foo",
parameter: map[string]interface{}{"foo": -8.0},
want: true,
},
{
name: "Fixed-point parameter",
expression: "foo > 1",
parameter: map[string]interface{}{"foo": 2},
want: true,
},
{
name: "Modifier after closing clause",
expression: "(2 + 2) + 2 == 6",
want: true,
},
{
name: "Comparator after closing clause",
expression: "(2 + 2) >= 4",
want: true,
},
{
name: "Two-boolean logical operation (for issue #8)",
expression: "(foo == true) || (bar == true)",
parameter: map[string]interface{}{
"foo": true,
"bar": false,
},
want: true,
},
{
name: "Two-variable integer logical operation (for issue #8)",
expression: "foo > 10 && bar > 10",
parameter: map[string]interface{}{
"foo": 1,
"bar": 11,
},
want: false,
},
{
name: "Regex against right-hand parameter",
expression: `"foobar" =~ foo`,
parameter: map[string]interface{}{
"foo": "obar",
},
want: true,
},
{
name: "Not-regex against right-hand parameter",
expression: `"foobar" !~ foo`,
parameter: map[string]interface{}{
"foo": "baz",
},
want: true,
},
{
name: "Regex against two parameter",
expression: `foo =~ bar`,
parameter: map[string]interface{}{
"foo": "foobar",
"bar": "oba",
},
want: true,
},
{
name: "Not-regex against two parameter",
expression: "foo !~ bar",
parameter: map[string]interface{}{
"foo": "foobar",
"bar": "baz",
},
want: true,
},
{
name: "Pre-compiled regex",
expression: "foo =~ bar",
parameter: map[string]interface{}{
"foo": "foobar",
"bar": regexp.MustCompile("[fF][oO]+"),
},
want: true,
},
{
name: "Pre-compiled not-regex",
expression: "foo !~ bar",
parameter: map[string]interface{}{
"foo": "foobar",
"bar": regexp.MustCompile("[fF][oO]+"),
},
want: false,
},
{
name: "Single boolean parameter",
expression: "commission ? 10",
parameter: map[string]interface{}{
"commission": true},
want: 10.0,
},
{
name: "True comparator with a parameter",
expression: `partner == "amazon" ? 10`,
parameter: map[string]interface{}{
"partner": "amazon"},
want: 10.0,
},
{
name: "False comparator with a parameter",
expression: `partner == "amazon" ? 10`,
parameter: map[string]interface{}{
"partner": "ebay"},
want: nil,
},
{
name: "True comparator with multiple parameters",
expression: "theft && period == 24 ? 60",
parameter: map[string]interface{}{
"theft": true,
"period": 24,
},
want: 60.0,
},
{
name: "False comparator with multiple parameters",
expression: "theft && period == 24 ? 60",
parameter: map[string]interface{}{
"theft": false,
"period": 24,
},
want: nil,
},
{
name: "String concat with single string parameter",
expression: `foo + "bar"`,
parameter: map[string]interface{}{
"foo": "baz"},
want: "bazbar",
},
{
name: "String concat with multiple string parameter",
expression: "foo + bar",
parameter: map[string]interface{}{
"foo": "baz",
"bar": "quux",
},
want: "bazquux",
},
{
name: "String concat with float parameter",
expression: "foo + bar",
parameter: map[string]interface{}{
"foo": "baz",
"bar": 123.0,
},
want: "baz123",
},
{
name: "Mixed multiple string concat",
expression: `foo + 123 + "bar" + true`,
parameter: map[string]interface{}{"foo": "baz"},
want: "baz123bartrue",
},
{
name: "Integer width spectrum",
expression: "uint8 + uint16 + uint32 + uint64 + int8 + int16 + int32 + int64",
parameter: map[string]interface{}{
"uint8": uint8(0),
"uint16": uint16(0),
"uint32": uint32(0),
"uint64": uint64(0),
"int8": int8(0),
"int16": int16(0),
"int32": int32(0),
"int64": int64(0),
},
want: 0.0,
},
{
name: "Null coalesce right",
expression: "foo ?? 1.0",
parameter: map[string]interface{}{"foo": nil},
want: 1.0,
},
{
name: "Multiple comparator/logical operators (#30)",
expression: "(foo >= 2887057408 && foo <= 2887122943) || (foo >= 168100864 && foo <= 168118271)",
parameter: map[string]interface{}{"foo": 2887057409},
want: true,
},
{
name: "Multiple comparator/logical operators, opposite order (#30)",
expression: "(foo >= 168100864 && foo <= 168118271) || (foo >= 2887057408 && foo <= 2887122943)",
parameter: map[string]interface{}{"foo": 2887057409},
want: true,
},
{
name: "Multiple comparator/logical operators, small value (#30)",
expression: "(foo >= 2887057408 && foo <= 2887122943) || (foo >= 168100864 && foo <= 168118271)",
parameter: map[string]interface{}{"foo": 168100865},
want: true,
},
{
name: "Multiple comparator/logical operators, small value, opposite order (#30)",
expression: "(foo >= 168100864 && foo <= 168118271) || (foo >= 2887057408 && foo <= 2887122943)",
parameter: map[string]interface{}{"foo": 168100865},
want: true,
},
{
name: "Incomparable array equality comparison",
expression: "arr == arr",
parameter: map[string]interface{}{"arr": []int{0, 0, 0}},
want: true,
},
{
name: "Incomparable array not-equality comparison",
expression: "arr != arr",
parameter: map[string]interface{}{"arr": []int{0, 0, 0}},
want: false,
},
{
name: "Mixed function and parameters",
expression: "sum(1.2, amount) + name",
extension: Function("sum", func(arguments ...interface{}) (interface{}, error) {
sum := 0.0
for _, v := range arguments {
sum += v.(float64)
}
return sum, nil
},
),
parameter: map[string]interface{}{"amount": .8,
"name": "awesome",
},
want: "2awesome",
},
{
name: "Short-circuit OR",
expression: "true || fail()",
extension: Function("fail", func(arguments ...interface{}) (interface{}, error) {
return nil, fmt.Errorf("Did not short-circuit")
}),
want: true,
},
{
name: "Short-circuit AND",
expression: "false && fail()",
extension: Function("fail", func(arguments ...interface{}) (interface{}, error) {
return nil, fmt.Errorf("Did not short-circuit")
}),
want: false,
},
{
name: "Short-circuit ternary",
expression: "true ? 1 : fail()",
extension: Function("fail", func(arguments ...interface{}) (interface{}, error) {
return nil, fmt.Errorf("Did not short-circuit")
}),
want: 1.0,
},
{
name: "Short-circuit coalesce",
expression: `"foo" ?? fail()`,
extension: Function("fail", func(arguments ...interface{}) (interface{}, error) {
return nil, fmt.Errorf("Did not short-circuit")
}),
want: "foo",
},
{
name: "Simple parameter call",
expression: "foo.String",
parameter: map[string]interface{}{"foo": foo},
want: foo.String,
},
{
name: "Simple parameter function call",
expression: "foo.Func()",
parameter: map[string]interface{}{"foo": foo},
want: "funk",
},
{
name: "Simple parameter call from pointer",
expression: "fooptr.String",
parameter: map[string]interface{}{"fooptr": &foo},
want: foo.String,
},
{
name: "Simple parameter function call from pointer",
expression: "fooptr.Func()",
parameter: map[string]interface{}{"fooptr": &foo},
want: "funk",
},
{
name: "Simple parameter call",
expression: `foo.String == "hi"`,
parameter: map[string]interface{}{"foo": foo},
want: false,
},
{
name: "Simple parameter call with modifier",
expression: `foo.String + "hi"`,
parameter: map[string]interface{}{"foo": foo},
want: foo.String + "hi",
},
{
name: "Simple parameter function call, two-arg return",
expression: `foo.Func2()`,
parameter: map[string]interface{}{"foo": foo},
want: "frink",
},
{
name: "Simple parameter function call, one arg",
expression: `foo.FuncArgStr("boop")`,
parameter: map[string]interface{}{"foo": foo},
want: "boop",
},
{
name: "Simple parameter function call, one arg",
expression: `foo.FuncArgStr("boop") + "hi"`,
parameter: map[string]interface{}{"foo": foo},
want: "boophi",
},
{
name: "Nested parameter function call",
expression: `foo.Nested.Dunk("boop")`,
parameter: map[string]interface{}{"foo": foo},
want: "boopdunk",
},
{
name: "Nested parameter call",
expression: "foo.Nested.Funk",
parameter: map[string]interface{}{"foo": foo},
want: "funkalicious",
},
{
name: "Nested map call",
expression: `foo.Nested.Map["a"]`,
parameter: map[string]interface{}{"foo": foo},
want: 1,
},
{
name: "Nested slice call",
expression: `foo.Nested.Slice[1]`,
parameter: map[string]interface{}{"foo": foo},
want: 2,
},
{
name: "Parameter call with + modifier",
expression: "1 + foo.Int",
parameter: map[string]interface{}{"foo": foo},
want: 102.0,
},
{
name: "Parameter string call with + modifier",
expression: `"woop" + (foo.String)`,
parameter: map[string]interface{}{"foo": foo},
want: "woopstring!",
},
{
name: "Parameter call with && operator",
expression: "true && foo.BoolFalse",
parameter: map[string]interface{}{"foo": foo},
want: false,
},
{
name: "Null coalesce nested parameter",
expression: "foo.Nil ?? false",
parameter: map[string]interface{}{"foo": foo},
want: false,
},
{
name: "input functions",
expression: "func1() + func2()",
parameter: map[string]interface{}{
"func1": func() float64 { return 2000 },
"func2": func() float64 { return 2001 },
},
want: 4001.0,
},
{
name: "input functions",
expression: "func1(date1) + func2(date2)",
parameter: map[string]interface{}{
"date1": func() interface{} {
y2k, _ := time.Parse("2006", "2000")
return y2k
}(),
"date2": func() interface{} {
y2k1, _ := time.Parse("2006", "2001")
return y2k1
}(),
},
extension: NewLanguage(
Function("func1", func(arguments ...interface{}) (interface{}, error) {
return float64(arguments[0].(time.Time).Year()), nil
}),
Function("func2", func(arguments ...interface{}) (interface{}, error) {
return float64(arguments[0].(time.Time).Year()), nil
}),
),
want: 4001.0,
},
{
name: "complex64 number as parameter",
expression: "complex64",
parameter: map[string]interface{}{
"complex64": complex64(0),
"complex128": complex128(0),
},
want: complex64(0),
},
{
name: "complex128 number as parameter",
expression: "complex128",
parameter: map[string]interface{}{
"complex64": complex64(0),
"complex128": complex128(0),
},
want: complex128(0),
},
{
name: "coalesce with undefined",
expression: "fooz ?? foo",
parameter: map[string]interface{}{
"foo": "bar",
},
want: "bar",
},
{
name: "map[interface{}]interface{}",
expression: "foo",
parameter: map[interface{}]interface{}{
"foo": "bar",
},
want: "bar",
},
{
name: "method on pointer type",
expression: "foo.PointerFunc()",
parameter: map[string]interface{}{
"foo": &dummyParameter{},
},
want: "point",
},
{
name: "custom selector",
expression: "hello.world",
parameter: "!",
extension: NewLanguage(Base(), VariableSelector(func(path Evaluables) Evaluable {
return func(c context.Context, v interface{}) (interface{}, error) {
keys, err := path.EvalStrings(c, v)
if err != nil {
return nil, err
}
return fmt.Sprintf("%s%s", strings.Join(keys, " "), v), nil
}
})),
want: "hello world!",
},
{
name: "map[int]int",
expression: `a[0] + a[2]`,
parameter: map[string]interface{}{
"a": map[int]int{0: 1, 2: 1},
},
want: 2.,
},
{
name: "map[int]string",
expression: `a[0] * a[2]`,
parameter: map[string]interface{}{
"a": map[int]string{0: "1", 2: "1"},
},
want: 1.,
},
{
name: "coalesce typed nil 0",
expression: `ProjectID ?? 0`,
parameter: struct {
ProjectID *uint
}{},
want: 0.,
},
{
name: "coalesce typed nil 99",
expression: `ProjectID ?? 99`,
parameter: struct {
ProjectID *uint
}{},
want: 99.,
},
{
name: "operator with typed nil 99",
expression: `ProjectID + 99`,
parameter: struct {
ProjectID *uint
}{},
want: "<nil>99",
},
{
name: "operator with typed nil if",
expression: `Flag ? 1 : 2`,
parameter: struct {
Flag *uint
}{},
want: 2.,
},
{
name: "Decimal math doesn't experience rounding error",
expression: "(x * 12.146) - y",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: decimal.NewFromFloat(156.825),
equalityFunc: decimalEqualityFunc,
},
{
name: "Decimal logical operators fractional difference",
expression: "((x * 12.146) - y) > 156.824999999",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: true,
},
{
name: "Decimal logical operators whole number difference",
expression: "((x * 12.146) - y) > 156",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: true,
},
{
name: "Decimal logical operators exact decimal match against GT",
expression: "((x * 12.146) - y) > 156.825",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: false,
},
{
name: "Decimal logical operators exact equality",
expression: "((x * 12.146) - y) == 156.825",
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
},
want: true,
},
{
name: "Decimal mixes with string logic with force fail",
expression: `(((x * 12.146) - y) == 156.825) && a == "test" && !b && b`,
extension: decimalArithmetic,
parameter: map[string]interface{}{
"x": 12.5,
"y": -5,
"a": "test",
"b": false,
},
want: false,
},
{
name: "Typed map with function call",
expression: `foo.MapWithFunc.Sum("a")`,
parameter: map[string]interface{}{
"foo": foo,
},
want: 3,
},
{
name: "Types slice with function call",
expression: `foo.SliceWithFunc.Sum("a")`,
parameter: map[string]interface{}{
"foo": foo,
},
want: 2,
},
},
t,
)
}

View File

@ -1,179 +0,0 @@
package gval
import (
"regexp/syntax"
"testing"
)
func TestParsingFailure(t *testing.T) {
testEvaluate(
[]evaluationTest{
{
name: "Invalid equality comparator",
expression: "1 = 1",
wantErr: unexpected(`"="`, "operator"),
},
{
name: "Invalid equality comparator",
expression: "1 === 1",
wantErr: unexpected(`"="`, "extension"),
},
{
name: "Too many characters for logical operator",
expression: "true &&& false",
wantErr: unexpected(`"&"`, "extension"),
},
{
name: "Too many characters for logical operator",
expression: "true ||| false",
wantErr: unexpected(`"|"`, "extension"),
},
{
name: "Premature end to expression, via modifier",
expression: "10 > 5 +",
wantErr: unexpected("EOF", "extensions"),
},
{
name: "Premature end to expression, via comparator",
expression: "10 + 5 >",
wantErr: unexpected("EOF", "extensions"),
},
{
name: "Premature end to expression, via logical operator",
expression: "10 > 5 &&",
wantErr: unexpected("EOF", "extensions"),
},
{
name: "Premature end to expression, via ternary operator",
expression: "true ?",
wantErr: unexpected("EOF", "extensions"),
},
{
name: "Hanging REQ",
expression: "`wat` =~",
wantErr: unexpected("EOF", "extensions"),
},
{
name: "Invalid operator change to REQ",
expression: " / =~",
wantErr: unexpected(`"/"`, "extensions"),
},
{
name: "Invalid starting token, comparator",
expression: "> 10",
wantErr: unexpected(`">"`, "extensions"),
},
{
name: "Invalid starting token, modifier",
expression: "+ 5",
wantErr: unexpected(`"+"`, "extensions"),
},
{
name: "Invalid starting token, logical operator",
expression: "&& 5 < 10",
wantErr: unexpected(`"&"`, "extensions"),
},
{
name: "Invalid NUMERIC transition",
expression: "10 10",
wantErr: unexpected(`Int`, "operator"),
},
{
name: "Invalid STRING transition",
expression: "`foo` `foo`",
wantErr: `String while scanning operator`, // can't use func unexpected because the token was changed from String to RawString in go 1.11
},
{
name: "Invalid operator transition",
expression: "10 > < 10",
wantErr: unexpected(`"<"`, "extensions"),
},
{
name: "Starting with unbalanced parens",
expression: " ) ( arg2",
wantErr: unexpected(`")"`, "extensions"),
},
{
name: "Unclosed bracket",
expression: "[foo bar",
wantErr: unexpected(`EOF`, "extensions"),
},
{
name: "Unclosed quote",
expression: "foo == `responseTime",
wantErr: "could not parse string",
},
{
name: "Constant regex pattern fail to compile",
expression: "foo =~ `[abc`",
wantErr: string(syntax.ErrMissingBracket),
},
{
name: "Constant unmatch regex pattern fail to compile",
expression: "foo !~ `[abc`",
wantErr: string(syntax.ErrMissingBracket),
},
{
name: "Unbalanced parentheses",
expression: "10 > (1 + 50",
wantErr: unexpected(`EOF`, "parentheses"),
},
{
name: "Multiple radix",
expression: "127.0.0.1",
wantErr: unexpected(`Float`, "operator"),
},
{
name: "Hanging accessor",
expression: "foo.Bar.",
wantErr: unexpected(`EOF`, "field"),
},
{
name: "Incomplete Hex",
expression: "0x",
wantErr: `strconv.ParseFloat: parsing "0x": invalid syntax`,
},
{
name: "Invalid Hex literal",
expression: "0x > 0",
wantErr: `strconv.ParseFloat: parsing "0x": invalid syntax`,
},
{
name: "Hex float (Unsupported)",
expression: "0x1.1",
wantErr: `strconv.ParseFloat: parsing "0x1.1": invalid syntax`,
},
{
name: "Hex invalid letter",
expression: "0x12g1",
wantErr: `strconv.ParseFloat: parsing "0x12": invalid syntax`,
},
{
name: "Error after camouflage",
expression: "0 + ,",
wantErr: `unexpected "," while scanning extensions`,
},
},
t,
)
}
func unknownOp(op string) string {
return "unknown operator " + op
}
func unexpected(token, unit string) string {
return "unexpected " + token + " while scanning " + unit
}

View File

@ -1,153 +0,0 @@
package gval
import (
"fmt"
"reflect"
"strings"
"testing"
"github.com/shopspring/decimal"
)
type evaluationTest struct {
name string
expression string
extension Language
parameter interface{}
want interface{}
equalityFunc func(x, y interface{}) bool
wantErr string
}
func testEvaluate(tests []evaluationTest, t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := Evaluate(tt.expression, tt.parameter, tt.extension)
if tt.wantErr != "" {
if err == nil {
t.Fatalf("Evaluate(%s) expected error but got %v", tt.expression, got)
}
if !strings.Contains(err.Error(), tt.wantErr) {
t.Fatalf("Evaluate(%s) expected error %s but got error %v", tt.expression, tt.wantErr, err)
}
return
}
if err != nil {
t.Errorf("Evaluate() error = %v", err)
return
}
if ef := tt.equalityFunc; ef != nil {
if !ef(got, tt.want) {
t.Errorf("Evaluate(%s) = %v, want %v", tt.expression, got, tt.want)
}
} else if !reflect.DeepEqual(got, tt.want) {
t.Errorf("Evaluate(%s) = %v, want %v", tt.expression, got, tt.want)
}
})
}
}
// dummyParameter used to test "parameter calls".
type dummyParameter struct {
String string
Int int
BoolFalse bool
Nil interface{}
Nested dummyNestedParameter
MapWithFunc dummyMapWithFunc
SliceWithFunc dummySliceWithFunc
}
func (d dummyParameter) Func() string {
return "funk"
}
func (d dummyParameter) Func2() (string, error) {
return "frink", nil
}
func (d *dummyParameter) PointerFunc() (string, error) {
return "point", nil
}
func (d dummyParameter) FuncErr() (string, error) {
return "", fmt.Errorf("fumps")
}
func (d dummyParameter) FuncArgStr(arg1 string) string {
return arg1
}
func (d dummyParameter) AlwaysFail() (interface{}, error) {
return nil, fmt.Errorf("function should always fail")
}
type dummyNestedParameter struct {
Funk string
Map map[string]int
Slice []int
}
func (d dummyNestedParameter) Dunk(arg1 string) string {
return arg1 + "dunk"
}
var foo = dummyParameter{
String: "string!",
Int: 101,
BoolFalse: false,
Nil: nil,
Nested: dummyNestedParameter{
Funk: "funkalicious",
Map: map[string]int{"a": 1, "b": 2, "c": 3},
Slice: []int{1, 2, 3},
},
MapWithFunc: dummyMapWithFunc{"a": {1, 2}, "b": {3, 4}},
SliceWithFunc: dummySliceWithFunc{"a", "b", "c", "a"},
}
var fooFailureParameters = map[string]interface{}{
"foo": foo,
"fooptr": &foo,
}
var decimalEqualityFunc = func(x, y interface{}) bool {
v1, ok1 := x.(decimal.Decimal)
v2, ok2 := y.(decimal.Decimal)
if !ok1 || !ok2 {
return false
}
return v1.Equal(v2)
}
type dummyMapWithFunc map[string][]int
func (m dummyMapWithFunc) Sum(key string) int {
values, ok := m[key]
if !ok {
return -1
}
sum := 0
for _, v := range values {
sum += v
}
return sum
}
type dummySliceWithFunc []string
func (m dummySliceWithFunc) Sum(key string) int {
sum := 0
for _, v := range m {
if v == key {
sum += 1
}
}
return sum
}

View File

@ -1,281 +0,0 @@
package gval
import (
"context"
"fmt"
"text/scanner"
"unicode"
"github.com/shopspring/decimal"
)
// Language is an expression language
type Language struct {
prefixes map[interface{}]extension
operators map[string]operator
operatorSymbols map[rune]struct{}
init extension
def extension
selector func(Evaluables) Evaluable
}
// NewLanguage returns the union of given Languages as new Language.
func NewLanguage(bases ...Language) Language {
l := newLanguage()
for _, base := range bases {
for i, e := range base.prefixes {
l.prefixes[i] = e
}
for i, e := range base.operators {
l.operators[i] = e.merge(l.operators[i])
l.operators[i].initiate(i)
}
for i := range base.operatorSymbols {
l.operatorSymbols[i] = struct{}{}
}
if base.init != nil {
l.init = base.init
}
if base.def != nil {
l.def = base.def
}
if base.selector != nil {
l.selector = base.selector
}
}
return l
}
func newLanguage() Language {
return Language{
prefixes: map[interface{}]extension{},
operators: map[string]operator{},
operatorSymbols: map[rune]struct{}{},
}
}
// NewEvaluable returns an Evaluable for given expression in the specified language
func (l Language) NewEvaluable(expression string) (Evaluable, error) {
return l.NewEvaluableWithContext(context.Background(), expression)
}
// NewEvaluableWithContext returns an Evaluable for given expression in the specified language using context
func (l Language) NewEvaluableWithContext(c context.Context, expression string) (Evaluable, error) {
p := newParser(expression, l)
eval, err := p.parse(c)
if err == nil && p.isCamouflaged() && p.lastScan != scanner.EOF {
err = p.camouflage
}
if err != nil {
pos := p.scanner.Pos()
return nil, fmt.Errorf("parsing error: %s - %d:%d %w", p.scanner.Position, pos.Line, pos.Column, err)
}
return eval, nil
}
// Evaluate given parameter with given expression
func (l Language) Evaluate(expression string, parameter interface{}) (interface{}, error) {
return l.EvaluateWithContext(context.Background(), expression, parameter)
}
// Evaluate given parameter with given expression using context
func (l Language) EvaluateWithContext(c context.Context, expression string, parameter interface{}) (interface{}, error) {
eval, err := l.NewEvaluableWithContext(c, expression)
if err != nil {
return nil, err
}
v, err := eval(c, parameter)
if err != nil {
return nil, fmt.Errorf("can not evaluate %s: %w", expression, err)
}
return v, nil
}
// Function returns a Language with given function.
// Function has no conversion for input types.
//
// If the function returns an error it must be the last return parameter.
//
// If the function has (without the error) more then one return parameter,
// it returns them as []interface{}.
func Function(name string, function interface{}) Language {
l := newLanguage()
l.prefixes[name] = func(c context.Context, p *Parser) (eval Evaluable, err error) {
args := []Evaluable{}
scan := p.Scan()
switch scan {
case '(':
args, err = p.parseArguments(c)
if err != nil {
return nil, err
}
default:
p.Camouflage("function call", '(')
}
return p.callFunc(toFunc(function), args...), nil
}
return l
}
// Constant returns a Language with given constant
func Constant(name string, value interface{}) Language {
l := newLanguage()
l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (eval Evaluable, err error) {
return p.Const(value), nil
}
return l
}
// PrefixExtension extends a Language
func PrefixExtension(r rune, ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.prefixes[r] = ext
return l
}
// Init is a language that does no parsing, but invokes the given function when
// parsing starts. It is incumbent upon the function to call ParseExpression to
// continue parsing.
//
// This function can be used to customize the parser settings, such as
// whitespace or ident behavior.
func Init(ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.init = ext
return l
}
// DefaultExtension is a language that runs the given function if no other
// prefix matches.
func DefaultExtension(ext func(context.Context, *Parser) (Evaluable, error)) Language {
l := newLanguage()
l.def = ext
return l
}
// PrefixMetaPrefix chooses a Prefix to be executed
func PrefixMetaPrefix(r rune, ext func(context.Context, *Parser) (call string, alternative func() (Evaluable, error), err error)) Language {
l := newLanguage()
l.prefixes[r] = func(c context.Context, p *Parser) (Evaluable, error) {
call, alternative, err := ext(c, p)
if err != nil {
return nil, err
}
if prefix, ok := p.prefixes[l.makePrefixKey(call)]; ok {
return prefix(c, p)
}
return alternative()
}
return l
}
// PrefixOperator returns a Language with given prefix
func PrefixOperator(name string, e Evaluable) Language {
l := newLanguage()
l.prefixes[l.makePrefixKey(name)] = func(c context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseNextExpression(c)
if err != nil {
return nil, err
}
prefix := func(c context.Context, v interface{}) (interface{}, error) {
a, err := eval(c, v)
if err != nil {
return nil, err
}
return e(c, a)
}
if eval.IsConst() {
v, err := prefix(c, nil)
if err != nil {
return nil, err
}
prefix = p.Const(v)
}
return prefix, nil
}
return l
}
// PostfixOperator extends a Language.
func PostfixOperator(name string, ext func(context.Context, *Parser, Evaluable) (Evaluable, error)) Language {
l := newLanguage()
l.operators[l.makeInfixKey(name)] = postfix{
f: func(c context.Context, p *Parser, eval Evaluable, pre operatorPrecedence) (Evaluable, error) {
return ext(c, p, eval)
},
}
return l
}
// InfixOperator for two arbitrary values.
func InfixOperator(name string, f func(a, b interface{}) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{arbitrary: f})
}
// InfixShortCircuit operator is called after the left operand is evaluated.
func InfixShortCircuit(name string, f func(a interface{}) (interface{}, bool)) Language {
return newLanguageOperator(name, &infix{shortCircuit: f})
}
// InfixTextOperator for two text values.
func InfixTextOperator(name string, f func(a, b string) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{text: f})
}
// InfixNumberOperator for two number values.
func InfixNumberOperator(name string, f func(a, b uint) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{number: f})
}
// InfixDecimalOperator for two decimal values.
func InfixDecimalOperator(name string, f func(a, b decimal.Decimal) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{decimal: f})
}
// InfixBoolOperator for two bool values.
func InfixBoolOperator(name string, f func(a, b bool) (interface{}, error)) Language {
return newLanguageOperator(name, &infix{boolean: f})
}
// Precedence of operator. The Operator with higher operatorPrecedence is evaluated first.
func Precedence(name string, operatorPrecendence uint8) Language {
return newLanguageOperator(name, operatorPrecedence(operatorPrecendence))
}
// InfixEvalOperator operates on the raw operands.
// Therefore it cannot be combined with operators for other operand types.
func InfixEvalOperator(name string, f func(a, b Evaluable) (Evaluable, error)) Language {
return newLanguageOperator(name, directInfix{infixBuilder: f})
}
func newLanguageOperator(name string, op operator) Language {
op.initiate(name)
l := newLanguage()
l.operators[l.makeInfixKey(name)] = op
return l
}
func (l *Language) makePrefixKey(key string) interface{} {
runes := []rune(key)
if len(runes) == 1 && !unicode.IsLetter(runes[0]) {
return runes[0]
}
return key
}
func (l *Language) makeInfixKey(key string) string {
for _, r := range key {
l.operatorSymbols[r] = struct{}{}
}
return key
}
// VariableSelector returns a Language which uses given variable selector.
// It must be combined with a Language that uses the vatiable selector. E.g. gval.Base().
func VariableSelector(selector func(path Evaluables) Evaluable) Language {
l := newLanguage()
l.selector = selector
return l
}

View File

@ -1,403 +0,0 @@
package gval
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
"github.com/shopspring/decimal"
)
type stage struct {
Evaluable
infixBuilder
operatorPrecedence
}
type stageStack []stage //operatorPrecedence in stacktStage is continuously, monotone ascending
func (s *stageStack) push(b stage) error {
for len(*s) > 0 && s.peek().operatorPrecedence >= b.operatorPrecedence {
a := s.pop()
eval, err := a.infixBuilder(a.Evaluable, b.Evaluable)
if err != nil {
return err
}
if a.IsConst() && b.IsConst() {
v, err := eval(nil, nil)
if err != nil {
return err
}
b.Evaluable = constant(v)
continue
}
b.Evaluable = eval
}
*s = append(*s, b)
return nil
}
func (s *stageStack) peek() stage {
return (*s)[len(*s)-1]
}
func (s *stageStack) pop() stage {
a := s.peek()
(*s) = (*s)[:len(*s)-1]
return a
}
type infixBuilder func(a, b Evaluable) (Evaluable, error)
func (l Language) isSymbolOperation(r rune) bool {
_, in := l.operatorSymbols[r]
return in
}
func (l Language) isOperatorPrefix(op string) bool {
for k := range l.operators {
if strings.HasPrefix(k, op) {
return true
}
}
return false
}
func (op *infix) initiate(name string) {
f := func(a, b interface{}) (interface{}, error) {
return nil, fmt.Errorf("invalid operation (%T) %s (%T)", a, name, b)
}
if op.arbitrary != nil {
f = op.arbitrary
}
for _, typeConvertion := range []bool{true, false} {
if op.text != nil && (!typeConvertion || op.arbitrary == nil) {
f = getStringOpFunc(op.text, f, typeConvertion)
}
if op.boolean != nil {
f = getBoolOpFunc(op.boolean, f, typeConvertion)
}
if op.number != nil {
f = getUintOpFunc(op.number, f, typeConvertion)
}
if op.decimal != nil {
f = getDecimalOpFunc(op.decimal, f, typeConvertion)
}
}
if op.shortCircuit == nil {
op.builder = func(a, b Evaluable) (Evaluable, error) {
return func(c context.Context, x interface{}) (interface{}, error) {
a, err := a(c, x)
if err != nil {
return nil, err
}
b, err := b(c, x)
if err != nil {
return nil, err
}
return f(a, b)
}, nil
}
return
}
shortF := op.shortCircuit
op.builder = func(a, b Evaluable) (Evaluable, error) {
return func(c context.Context, x interface{}) (interface{}, error) {
a, err := a(c, x)
if err != nil {
return nil, err
}
if r, ok := shortF(a); ok {
return r, nil
}
b, err := b(c, x)
if err != nil {
return nil, err
}
return f(a, b)
}, nil
}
}
type opFunc func(a, b interface{}) (interface{}, error)
func getStringOpFunc(s func(a, b string) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
if a != nil && b != nil {
return s(fmt.Sprintf("%v", a), fmt.Sprintf("%v", b))
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
s1, k := a.(string)
s2, l := b.(string)
if k && l {
return s(s1, s2)
}
return f(a, b)
}
}
func convertToBool(o interface{}) (bool, bool) {
if b, ok := o.(bool); ok {
return b, true
}
v := reflect.ValueOf(o)
if v.Kind() == reflect.Func {
if vt := v.Type(); vt.NumIn() == 0 && vt.NumOut() == 1 {
retType := vt.Out(0)
if retType.Kind() == reflect.Bool {
funcResults := v.Call([]reflect.Value{})
v = funcResults[0]
o = v.Interface()
}
}
}
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return false, false
}
o = v.Interface()
}
if o == false || o == nil || o == "false" || o == "FALSE" {
return false, true
}
if o == true || o == "true" || o == "TRUE" {
return true, true
}
if f, ok := convertToUint(o); ok {
return f != 0., true
}
return false, false
}
func getBoolOpFunc(o func(a, b bool) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToBool(a)
y, l := convertToBool(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(bool)
y, l := b.(bool)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
func convertToUint(o interface{}) (uint, bool) {
if i, ok := o.(uint); ok {
return i, true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return 0, false
}
o = v.Interface()
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return uint(v.Int()), true
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return uint(v.Uint()), true
}
if s, ok := o.(string); ok {
u, err := strconv.ParseUint(s, 0, 32)
if err == nil {
return uint(u), true
}
}
return 0, false
}
func getUintOpFunc(o func(a, b uint) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToUint(a)
y, l := convertToUint(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(uint)
y, l := b.(uint)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
func convertToDecimal(o interface{}) (decimal.Decimal, bool) {
if i, ok := o.(decimal.Decimal); ok {
return i, true
}
if i, ok := o.(float64); ok {
return decimal.NewFromFloat(i), true
}
v := reflect.ValueOf(o)
for o != nil && v.Kind() == reflect.Ptr {
v = v.Elem()
if !v.IsValid() {
return decimal.Zero, false
}
o = v.Interface()
}
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
return decimal.NewFromInt(v.Int()), true
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
return decimal.NewFromFloat(float64(v.Uint())), true
case reflect.Float32, reflect.Float64:
return decimal.NewFromFloat(v.Float()), true
}
if s, ok := o.(string); ok {
f, err := strconv.ParseFloat(s, 64)
if err == nil {
return decimal.NewFromFloat(f), true
}
}
return decimal.Zero, false
}
func getDecimalOpFunc(o func(a, b decimal.Decimal) (interface{}, error), f opFunc, typeConversion bool) opFunc {
if typeConversion {
return func(a, b interface{}) (interface{}, error) {
x, k := convertToDecimal(a)
y, l := convertToDecimal(b)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
return func(a, b interface{}) (interface{}, error) {
x, k := a.(decimal.Decimal)
y, l := b.(decimal.Decimal)
if k && l {
return o(x, y)
}
return f(a, b)
}
}
type operator interface {
merge(operator) operator
precedence() operatorPrecedence
initiate(name string)
}
type operatorPrecedence uint8
func (pre operatorPrecedence) merge(op operator) operator {
if op, ok := op.(operatorPrecedence); ok {
if op > pre {
return op
}
return pre
}
if op == nil {
return pre
}
return op.merge(pre)
}
func (pre operatorPrecedence) precedence() operatorPrecedence {
return pre
}
func (pre operatorPrecedence) initiate(name string) {}
type infix struct {
operatorPrecedence
number func(a, b uint) (interface{}, error)
decimal func(a, b decimal.Decimal) (interface{}, error)
boolean func(a, b bool) (interface{}, error)
text func(a, b string) (interface{}, error)
arbitrary func(a, b interface{}) (interface{}, error)
shortCircuit func(a interface{}) (interface{}, bool)
builder infixBuilder
}
func (op infix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case *infix:
if op.number == nil {
op.number = op2.number
}
if op.decimal == nil {
op.decimal = op2.decimal
}
if op.boolean == nil {
op.boolean = op2.boolean
}
if op.text == nil {
op.text = op2.text
}
if op.arbitrary == nil {
op.arbitrary = op2.arbitrary
}
if op.shortCircuit == nil {
op.shortCircuit = op2.shortCircuit
}
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return &op
}
type directInfix struct {
operatorPrecedence
infixBuilder
}
func (op directInfix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case operatorPrecedence:
op.operatorPrecedence = op2
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return op
}
type extension func(context.Context, *Parser) (Evaluable, error)
type postfix struct {
operatorPrecedence
f func(context.Context, *Parser, Evaluable, operatorPrecedence) (Evaluable, error)
}
func (op postfix) merge(op2 operator) operator {
switch op2 := op2.(type) {
case postfix:
if op2.f != nil {
op.f = op2.f
}
}
if op2 != nil && op2.precedence() > op.operatorPrecedence {
op.operatorPrecedence = op2.precedence()
}
return op
}

View File

@ -1,166 +0,0 @@
package gval
import (
"context"
"fmt"
"reflect"
"testing"
)
func Test_Infix(t *testing.T) {
type subTest struct {
name string
a interface{}
b interface{}
wantRet interface{}
}
tests := []struct {
name string
infix
subTests []subTest
}{
{
"number operator",
infix{
number: func(a, b float64) (interface{}, error) { return a * b, nil },
},
[]subTest{
{"float64 arguments", 7., 3., 21.},
{"int arguments", 7, 3, 21.},
{"string arguments", "7", "3.", 21.},
},
},
{
"number and string operator",
infix{
number: func(a, b float64) (interface{}, error) { return a + b, nil },
text: func(a, b string) (interface{}, error) { return fmt.Sprintf("%v%v", a, b), nil },
},
[]subTest{
{"float64 arguments", 7., 3., 10.},
{"int arguments", 7, 3, 10.},
{"number string arguments", "7", "3.", "73."},
{"string arguments", "hello ", "world", "hello world"},
},
},
{
"bool operator",
infix{
shortCircuit: func(a interface{}) (interface{}, bool) { return false, a == false },
boolean: func(a, b bool) (interface{}, error) { return a && b, nil },
},
[]subTest{
{"bool arguments", false, true, false},
{"number arguments", 0, true, false},
{"lower string arguments", "false", "true", false},
{"upper string arguments", "TRUE", "FALSE", false},
{"shortCircuit", false, "not a boolean", false},
},
},
{
"bool, number, text and interface operator",
infix{
number: func(a, b float64) (interface{}, error) { return a == b, nil },
boolean: func(a, b bool) (interface{}, error) { return a == b, nil },
text: func(a, b string) (interface{}, error) { return a == b, nil },
arbitrary: func(a, b interface{}) (interface{}, error) { return a == b, nil },
},
[]subTest{
{"number string and int arguments", "7", 7, true},
{"bool string and bool arguments", "true", true, true},
{"string arguments", "hello", "hello", true},
{"upper string arguments", "TRUE", "FALSE", false},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
tt.infix.initiate("<" + tt.name + ">")
builder := tt.infix.builder
for _, tt := range tt.subTests {
t.Run(tt.name, func(t *testing.T) {
eval, err := builder(constant(tt.a), constant(tt.b))
if err != nil {
t.Fatal(err)
}
got, err := eval(context.Background(), nil)
if err != nil {
t.Fatal(err)
}
if !reflect.DeepEqual(got, tt.wantRet) {
t.Fatalf("binaryOperator() eval() = %v, want %v", got, tt.wantRet)
}
})
}
})
}
}
func Test_stageStack_push(t *testing.T) {
p := (*Parser)(nil)
tests := []struct {
name string
pres []operatorPrecedence
expect string
}{
{
"flat",
[]operatorPrecedence{1, 1, 1, 1},
"((((AB)C)D)E)",
},
{
"asc",
[]operatorPrecedence{1, 2, 3, 4},
"(A(B(C(DE))))",
},
{
"desc",
[]operatorPrecedence{4, 3, 2, 1},
"((((AB)C)D)E)",
},
{
"mixed",
[]operatorPrecedence{1, 2, 1, 1},
"(((A(BC))D)E)",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
X := int('A')
op := func(a, b Evaluable) (Evaluable, error) {
return func(c context.Context, o interface{}) (interface{}, error) {
aa, _ := a.EvalString(c, nil)
bb, _ := b.EvalString(c, nil)
s := "(" + aa + bb + ")"
return s, nil
}, nil
}
stack := stageStack{}
for _, pre := range tt.pres {
if err := stack.push(stage{p.Const(string(rune(X))), op, pre}); err != nil {
t.Fatal(err)
}
X++
}
if err := stack.push(stage{p.Const(string(rune(X))), nil, 0}); err != nil {
t.Fatal(err)
}
if len(stack) != 1 {
t.Fatalf("stack must hold exactly one element")
}
got, _ := stack[0].EvalString(context.Background(), nil)
if got != tt.expect {
t.Fatalf("got %s but expected %s", got, tt.expect)
}
})
}
}

View File

@ -1,347 +0,0 @@
package gval
import (
"context"
"fmt"
"reflect"
"strconv"
"text/scanner"
"github.com/shopspring/decimal"
)
// ParseExpression scans an expression into an Evaluable.
func (p *Parser) ParseExpression(c context.Context) (eval Evaluable, err error) {
stack := stageStack{}
for {
eval, err = p.ParseNextExpression(c)
if err != nil {
return nil, err
}
if stage, err := p.parseOperator(c, &stack, eval); err != nil {
return nil, err
} else if err = stack.push(stage); err != nil {
return nil, err
}
if stack.peek().infixBuilder == nil {
return stack.pop().Evaluable, nil
}
}
}
// ParseNextExpression scans the expression ignoring following operators
func (p *Parser) ParseNextExpression(c context.Context) (eval Evaluable, err error) {
scan := p.Scan()
ex, ok := p.prefixes[scan]
if !ok {
if scan != scanner.EOF && p.def != nil {
return p.def(c, p)
}
return nil, p.Expected("extensions")
}
return ex(c, p)
}
// ParseSublanguage sets the next language for this parser to parse and calls
// its initialization function, usually ParseExpression.
func (p *Parser) ParseSublanguage(c context.Context, l Language) (Evaluable, error) {
if p.isCamouflaged() {
panic("can not ParseSublanguage() on camouflaged Parser")
}
curLang := p.Language
curWhitespace := p.scanner.Whitespace
curMode := p.scanner.Mode
curIsIdentRune := p.scanner.IsIdentRune
p.Language = l
p.resetScannerProperties()
defer func() {
p.Language = curLang
p.scanner.Whitespace = curWhitespace
p.scanner.Mode = curMode
p.scanner.IsIdentRune = curIsIdentRune
}()
return p.parse(c)
}
func (p *Parser) parse(c context.Context) (Evaluable, error) {
if p.init != nil {
return p.init(c, p)
}
return p.ParseExpression(c)
}
func parseString(c context.Context, p *Parser) (Evaluable, error) {
s, err := strconv.Unquote(p.TokenText())
if err != nil {
return nil, fmt.Errorf("could not parse string: %w", err)
}
return p.Const(s), nil
}
func parseNumber(c context.Context, p *Parser) (Evaluable, error) {
n, err := strconv.ParseUint(p.TokenText(), 0, 32)
if err != nil {
return nil, err
}
return p.Const(n), nil
}
func parseDecimal(c context.Context, p *Parser) (Evaluable, error) {
n, err := strconv.ParseFloat(p.TokenText(), 64)
if err != nil {
return nil, err
}
return p.Const(decimal.NewFromFloat(n)), nil
}
func parseParentheses(c context.Context, p *Parser) (Evaluable, error) {
eval, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
switch p.Scan() {
case ')':
return eval, nil
default:
return nil, p.Expected("parentheses", ')')
}
}
func (p *Parser) parseOperator(c context.Context, stack *stageStack, eval Evaluable) (st stage, err error) {
for {
scan := p.Scan()
op := p.TokenText()
mustOp := false
if p.isSymbolOperation(scan) {
scan = p.Peek()
for p.isSymbolOperation(scan) && p.isOperatorPrefix(op+string(scan)) {
mustOp = true
op += string(scan)
p.Next()
scan = p.Peek()
}
} else if scan != scanner.Ident {
p.Camouflage("operator")
return stage{Evaluable: eval}, nil
}
switch operator := p.operators[op].(type) {
case *infix:
return stage{
Evaluable: eval,
infixBuilder: operator.builder,
operatorPrecedence: operator.operatorPrecedence,
}, nil
case directInfix:
return stage{
Evaluable: eval,
infixBuilder: operator.infixBuilder,
operatorPrecedence: operator.operatorPrecedence,
}, nil
case postfix:
if err = stack.push(stage{
operatorPrecedence: operator.operatorPrecedence,
Evaluable: eval,
}); err != nil {
return stage{}, err
}
eval, err = operator.f(c, p, stack.pop().Evaluable, operator.operatorPrecedence)
if err != nil {
return
}
continue
}
if !mustOp {
p.Camouflage("operator")
return stage{Evaluable: eval}, nil
}
return stage{}, fmt.Errorf("unknown operator %s", op)
}
}
func parseIdent(c context.Context, p *Parser) (call string, alternative func() (Evaluable, error), err error) {
token := p.TokenText()
return token,
func() (Evaluable, error) {
fullname := token
keys := []Evaluable{p.Const(token)}
for {
scan := p.Scan()
switch scan {
case '.':
scan = p.Scan()
switch scan {
case scanner.Ident:
token = p.TokenText()
keys = append(keys, p.Const(token))
default:
return nil, p.Expected("field", scanner.Ident)
}
case '(':
args, err := p.parseArguments(c)
if err != nil {
return nil, err
}
return p.callEvaluable(fullname, p.Var(keys...), args...), nil
case '[':
key, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
switch p.Scan() {
case ']':
keys = append(keys, key)
default:
return nil, p.Expected("array key", ']')
}
default:
p.Camouflage("variable", '.', '(', '[')
return p.Var(keys...), nil
}
}
}, nil
}
func (p *Parser) parseArguments(c context.Context) (args []Evaluable, err error) {
if p.Scan() == ')' {
return
}
p.Camouflage("scan arguments", ')')
for {
arg, err := p.ParseExpression(c)
args = append(args, arg)
if err != nil {
return nil, err
}
switch p.Scan() {
case ')':
return args, nil
case ',':
default:
return nil, p.Expected("arguments", ')', ',')
}
}
}
func inArray(a, b interface{}) (interface{}, error) {
col, ok := b.([]interface{})
if !ok {
return nil, fmt.Errorf("expected type []interface{} for in operator but got %T", b)
}
for _, value := range col {
if reflect.DeepEqual(a, value) {
return true, nil
}
}
return false, nil
}
func parseIf(c context.Context, p *Parser, e Evaluable) (Evaluable, error) {
a, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
b := p.Const(nil)
switch p.Scan() {
case ':':
b, err = p.ParseExpression(c)
if err != nil {
return nil, err
}
case scanner.EOF:
default:
return nil, p.Expected("<> ? <> : <>", ':', scanner.EOF)
}
return func(c context.Context, v interface{}) (interface{}, error) {
x, err := e(c, v)
if err != nil {
return nil, err
}
if valX := reflect.ValueOf(x); x == nil || valX.IsZero() {
return b(c, v)
}
return a(c, v)
}, nil
}
func parseJSONArray(c context.Context, p *Parser) (Evaluable, error) {
evals := []Evaluable{}
for {
switch p.Scan() {
default:
p.Camouflage("array", ',', ']')
eval, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
evals = append(evals, eval)
case ',':
case ']':
return func(c context.Context, v interface{}) (interface{}, error) {
vs := make([]interface{}, len(evals))
for i, e := range evals {
eval, err := e(c, v)
if err != nil {
return nil, err
}
vs[i] = eval
}
return vs, nil
}, nil
}
}
}
func parseJSONObject(c context.Context, p *Parser) (Evaluable, error) {
type kv struct {
key Evaluable
value Evaluable
}
evals := []kv{}
for {
switch p.Scan() {
default:
p.Camouflage("object", ',', '}')
key, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
if p.Scan() != ':' {
if err != nil {
return nil, p.Expected("object", ':')
}
}
value, err := p.ParseExpression(c)
if err != nil {
return nil, err
}
evals = append(evals, kv{key, value})
case ',':
case '}':
return func(c context.Context, v interface{}) (interface{}, error) {
vs := map[string]interface{}{}
for _, e := range evals {
value, err := e.value(c, v)
if err != nil {
return nil, err
}
key, err := e.key.EvalString(c, v)
if err != nil {
return nil, err
}
vs[key] = value
}
return vs, nil
}, nil
}
}
}

View File

@ -1,147 +0,0 @@
package gval
import (
"bytes"
"fmt"
"strings"
"text/scanner"
"unicode"
)
// Parser parses expressions in a Language into an Evaluable
type Parser struct {
scanner scanner.Scanner
Language
lastScan rune
camouflage error
}
func newParser(expression string, l Language) *Parser {
sc := scanner.Scanner{}
sc.Init(strings.NewReader(expression))
sc.Error = func(*scanner.Scanner, string) {}
sc.Filename = expression + "\t"
p := &Parser{scanner: sc, Language: l}
p.resetScannerProperties()
return p
}
func (p *Parser) resetScannerProperties() {
p.scanner.Whitespace = scanner.GoWhitespace
p.scanner.Mode = scanner.GoTokens
p.scanner.IsIdentRune = func(r rune, pos int) bool {
return unicode.IsLetter(r) || r == '_' || (pos > 0 && unicode.IsDigit(r))
}
}
// SetWhitespace sets the behavior of the whitespace matcher. The given
// characters must be less than or equal to 0x20 (' ').
func (p *Parser) SetWhitespace(chars ...rune) {
var mask uint64
for _, char := range chars {
mask |= 1 << uint(char)
}
p.scanner.Whitespace = mask
}
// SetMode sets the tokens that the underlying scanner will match.
func (p *Parser) SetMode(mode uint) {
p.scanner.Mode = mode
}
// SetIsIdentRuneFunc sets the function that matches ident characters in the
// underlying scanner.
func (p *Parser) SetIsIdentRuneFunc(fn func(ch rune, i int) bool) {
p.scanner.IsIdentRune = fn
}
// Scan reads the next token or Unicode character from source and returns it.
// It only recognizes tokens t for which the respective Mode bit (1<<-t) is set.
// It returns scanner.EOF at the end of the source.
func (p *Parser) Scan() rune {
if p.isCamouflaged() {
p.camouflage = nil
return p.lastScan
}
p.camouflage = nil
p.lastScan = p.scanner.Scan()
return p.lastScan
}
func (p *Parser) isCamouflaged() bool {
return p.camouflage != nil && p.camouflage != errCamouflageAfterNext
}
// Camouflage rewind the last Scan(). The Parser holds the camouflage error until
// the next Scan()
// Do not call Rewind() on a camouflaged Parser
func (p *Parser) Camouflage(unit string, expected ...rune) {
if p.isCamouflaged() {
panic(fmt.Errorf("can only Camouflage() after Scan(): %w", p.camouflage))
}
p.camouflage = p.Expected(unit, expected...)
}
// Peek returns the next Unicode character in the source without advancing
// the scanner. It returns EOF if the scanner's position is at the last
// character of the source.
// Do not call Peek() on a camouflaged Parser
func (p *Parser) Peek() rune {
if p.isCamouflaged() {
panic("can not Peek() on camouflaged Parser")
}
return p.scanner.Peek()
}
var errCamouflageAfterNext = fmt.Errorf("Camouflage() after Next()")
// Next reads and returns the next Unicode character.
// It returns EOF at the end of the source.
// Do not call Next() on a camouflaged Parser
func (p *Parser) Next() rune {
if p.isCamouflaged() {
panic("can not Next() on camouflaged Parser")
}
p.camouflage = errCamouflageAfterNext
return p.scanner.Next()
}
// TokenText returns the string corresponding to the most recently scanned token.
// Valid after calling Scan().
func (p *Parser) TokenText() string {
return p.scanner.TokenText()
}
// Expected returns an error signaling an unexpected Scan() result
func (p *Parser) Expected(unit string, expected ...rune) error {
return unexpectedRune{unit, expected, p.lastScan}
}
type unexpectedRune struct {
unit string
expected []rune
got rune
}
func (err unexpectedRune) Error() string {
exp := bytes.Buffer{}
runes := err.expected
switch len(runes) {
default:
for _, r := range runes[:len(runes)-2] {
exp.WriteString(scanner.TokenString(r))
exp.WriteString(", ")
}
fallthrough
case 2:
exp.WriteString(scanner.TokenString(runes[len(runes)-2]))
exp.WriteString(" or ")
fallthrough
case 1:
exp.WriteString(scanner.TokenString(runes[len(runes)-1]))
case 0:
return fmt.Sprintf("unexpected %s while scanning %s", scanner.TokenString(err.got), err.unit)
}
return fmt.Sprintf("unexpected %s while scanning %s expected %s", scanner.TokenString(err.got), err.unit, exp.String())
}

View File

@ -1,148 +0,0 @@
package gval
import (
"testing"
"text/scanner"
"unicode"
)
func TestParser_Scan(t *testing.T) {
tests := []struct {
name string
input string
Language
do func(p *Parser)
wantScan rune
wantToken string
wantPanic bool
}{
{
name: "camouflage",
input: "$abc",
do: func(p *Parser) {
p.Scan()
p.Camouflage("test")
},
wantScan: '$',
wantToken: "$",
},
{
name: "camouflage with next",
input: "$abc",
do: func(p *Parser) {
p.Scan()
p.Camouflage("test")
p.Next()
},
wantPanic: true,
},
{
name: "camouflage scan camouflage",
input: "$abc",
do: func(p *Parser) {
p.Scan()
p.Camouflage("test")
p.Scan()
p.Camouflage("test2")
},
wantScan: '$',
wantToken: "$",
},
{
name: "camouflage with peek",
input: "$abc",
do: func(p *Parser) {
p.Scan()
p.Camouflage("test")
p.Peek()
},
wantPanic: true,
},
{
name: "next and peek",
input: "$#abc",
do: func(p *Parser) {
p.Scan()
p.Next()
p.Peek()
},
wantScan: scanner.Ident,
wantToken: "abc",
},
{
name: "scan token camouflage token",
input: "abc",
do: func(p *Parser) {
p.Scan()
p.TokenText()
p.Camouflage("test")
},
wantScan: scanner.Ident,
wantToken: "abc",
},
{
name: "scan token peek camouflage token",
input: "abc",
do: func(p *Parser) {
p.Scan()
p.TokenText()
p.Peek()
p.Camouflage("test")
},
wantScan: scanner.Ident,
wantToken: "abc",
},
{
name: "tokenize all whitespace",
input: "foo\tbar\nbaz",
do: func(p *Parser) {
p.SetWhitespace()
p.Scan()
},
wantScan: '\t',
wantToken: "\t",
},
{
name: "custom ident",
input: "$#foo",
do: func(p *Parser) {
p.SetIsIdentRuneFunc(func(ch rune, i int) bool { return unicode.IsLetter(ch) || ch == '#' })
p.Scan()
},
wantScan: scanner.Ident,
wantToken: "#foo",
},
{
name: "do not scan idents",
input: "abc",
do: func(p *Parser) {
p.SetMode(scanner.GoTokens ^ scanner.ScanIdents)
p.Scan()
},
wantScan: 'b',
wantToken: "b",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer func() {
err := recover()
if err != nil && !tt.wantPanic {
t.Fatalf("unexpected panic: %v", err)
}
}()
p := newParser(tt.input, tt.Language)
tt.do(p)
if tt.wantPanic {
return
}
scan := p.Scan()
token := p.TokenText()
if scan != tt.wantScan || token != tt.wantToken {
t.Errorf("Parser.Scan() = %v (%v), want %v (%v)", scan, token, tt.wantScan, tt.wantToken)
}
})
}
}

View File

@ -1,124 +0,0 @@
package gval
// Courtesy of abrander
// ref: https://gist.github.com/abrander/fa05ae9b181b48ffe7afb12c961b6e90
import (
"fmt"
"math/rand"
"testing"
"time"
)
var (
hello = "hello"
empty struct{}
empty2 *string
values = []interface{}{
-1,
0,
12,
13,
"",
"hello",
&hello,
nil,
"nil",
empty,
empty2,
true,
false,
time.Now(),
rune('r'),
int64(34),
time.Duration(0),
"true",
"false",
"\ntrue\n",
"\nfalse\n",
"12",
"nil",
"arg1",
"arg2",
int(12),
int32(12),
int64(12),
complex(1.0, 1.0),
[]byte{0, 0, 0},
[]int{0, 0, 0},
[]string{},
"[]",
"{}",
"\"\"",
"\"12\"",
"\"hello\"",
".*",
"==",
"!=",
">",
">=",
"<",
"<=",
"=~",
"!~",
"in",
"&&",
"||",
"^",
"&",
"|",
">>",
"<<",
"+",
"-",
"*",
"/",
"%",
"**",
"-",
"!",
"~",
"?",
":",
"??",
"+",
"-",
"*",
"/",
"%",
"**",
"&",
"|",
"^",
">>",
"<<",
",",
"(",
")",
"[",
"]",
"\n",
"\000",
}
)
const SEED = 1487873697990155515
func BenchmarkRandom(bench *testing.B) {
rand.Seed(SEED)
for i := 0; i < bench.N; i++ {
num := rand.Intn(3) + 2
expression := ""
for n := 0; n < num; n++ {
expression += fmt.Sprintf(" %s", getRandom(values))
}
Evaluate(expression, nil)
}
}
func getRandom(haystack []interface{}) interface{} {
i := rand.Intn(len(haystack))
return haystack[i]
}

View File

@ -1,256 +0,0 @@
package main
import (
"context"
_ "embed"
"fmt"
"math"
"okemu/config"
"okemu/debug"
"okemu/debug/zrcp"
"okemu/forms"
"okemu/logger"
"okemu/okean240"
"runtime"
"sync/atomic"
"time"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/canvas"
"fyne.io/fyne/v2/widget"
"github.com/loov/hrtime"
"github.com/romychs/z80go/dis"
)
var Version = "v1.0.3"
var BuildTime = "2026-04-08"
const defaultTimerClkPeriod = 433
const defaultCpuClkPeriod = 310
const windowsTimerClkPeriod = 400
const windowsCpuClkPeriod = 300
const maxDelta = 3
const diffScale = 15.0
////go:embed hex/m80.hex
//var serialBytes []byte
////go:embed bin/jack.com
//var ramBytes []byte
func main() {
fmt.Printf("Starting Ocean-240.2 emulator %s build at %s\n", Version, BuildTime)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// base log init
logger.InitLogging()
// load config yml file
config.LoadConfig()
conf := config.GetConfig()
// Reconfigure logging by config values
logger.ReconfigureLogging(conf)
if runtime.GOOS == "windows" {
cpuClkPeriod.Store(windowsCpuClkPeriod)
timerClkPeriod.Store(windowsTimerClkPeriod)
} else {
cpuClkPeriod.Store(defaultCpuClkPeriod)
timerClkPeriod.Store(defaultTimerClkPeriod)
}
debugger := debug.NewDebugger()
computer := okean240.NewComputer(conf, debugger)
computer.AutoLoadFloppy()
disassm := dis.NewDisassembler(computer)
w, raster, label := forms.NewMainWindow(computer, conf, "Океан 240.2 "+Version)
//dezog := dzrp.NewDZRP(conf, debugger, disassm, computer)
dezog := zrcp.NewZRCP(conf, debugger, disassm, computer)
go cpuClock(computer, dezog)
go timerClock(computer)
go screen(ctx, computer, raster, label)
if conf.Debugger.Enabled {
go dezog.SetupTcpHandler()
}
(*w).ShowAndRun()
computer.AutoSaveFloppy()
logger.CloseLogs()
}
func screen(ctx context.Context, computer *okean240.ComputerType, raster *canvas.Raster, label *widget.Label) {
ticker := time.NewTicker(20 * time.Millisecond)
defer ticker.Stop()
frame := 0
var pre uint64 = 0
var preTim uint64 = 0
var cpuFreq float64 = 0
var timerFreq float64 = 0
timeStart := hrtime.Now()
for {
select {
case <-ticker.C:
frame++
// redraw screen here
fyne.Do(func() {
// status for every 50 frames
if frame%10 == 0 {
timeElapsed := hrtime.Since(timeStart)
period := float64(timeElapsed.Nanoseconds()) / 1_000_000.0
cpuFreq = math.Round(float64(cpuTicks.Load()-pre)/period) / 1000.0
timerFreq = math.Round(float64(timerTicks.Load()-preTim)/period) / 1000.0
adjustPeriods(computer, cpuFreq, timerFreq)
if frame%50 == 0 {
label.SetText(formatLabel(computer, cpuFreq, timerFreq))
//log.Debugf("Cpu clk period: %d, Timer clock period: %d, frame time: %1.3fms", cpuClkPeriod.Load(), timerClkPeriod.Load(), period/5.0)
//logger.FlushLogs()
}
pre = cpuTicks.Load()
preTim = timerTicks.Load()
timeStart = hrtime.Now()
}
raster.Refresh()
})
case <-ctx.Done():
return
}
}
}
// adjustPeriods Adjust periods for CPU and Timer clock frequencies
func adjustPeriods(c *okean240.ComputerType, cpuFreq float64, timerFreq float64) {
// adjust cpu clock if not full speed
if !c.FullSpeed() {
calcPeriod(cpuFreq, okean240.CPUFrequency, okean240.CPUFrequencyHi, okean240.CPUFrequencyLow, &cpuClkPeriod)
}
// adjust timerClock clock
calcPeriod(timerFreq, okean240.TimerFrequency, okean240.TimerFrequencyHi, okean240.TimerFrequencyLow, &timerClkPeriod)
}
// calcPeriod calc new value period to adjust frequency of timer or CPU
func calcPeriod(curFreq float64, destFreq float64, hiLimit float64, loLimit float64, period *atomic.Int64) {
if curFreq > hiLimit && period.Load() < 2000 {
period.Add(calcDelta(curFreq, destFreq))
} else if curFreq < loLimit && period.Load() > 0 {
period.Add(-calcDelta(curFreq, destFreq))
if period.Load() < 0 {
period.Store(0)
}
}
}
// calcDelta calculate step to change period
func calcDelta(currentFreq float64, destFreq float64) int64 {
delta := int64(math.Round(math.Abs(destFreq-currentFreq) * diffScale))
if delta < 1 {
return 1
} else if delta > maxDelta {
return maxDelta
}
return delta
}
func formatLabel(computer *okean240.ComputerType, freq float64, freqTim float64) string {
return fmt.Sprintf("Screen size: %dx%d | Fcpu: %1.3fMHz | Ftmr: %1.3fMHz | Debugger: %s", // CP:%d TP:%d",
computer.ScreenWidth(), computer.ScreenHeight(), freq, freqTim, computer.DebuggerState())
}
var timerTicks atomic.Uint64
var timerClkPeriod atomic.Int64 // period in nanos for 1.5MHz frequency
var cpuClkPeriod atomic.Int64 // period in nanos for 2.5MHz frequency
func timerClock(computer *okean240.ComputerType) {
timeStart := hrtime.Now()
for {
elapsed := hrtime.Since(timeStart)
if int64(elapsed) > timerClkPeriod.Load() {
timeStart = hrtime.Now()
computer.TimerClk()
timerTicks.Add(1)
runtime.Gosched()
}
}
}
var cpuTicks atomic.Uint64
func cpuClock(computer *okean240.ComputerType, dezog debug.DEZOG) {
cpuTicks.Store(0)
nextTick := uint64(0)
var bp uint16
var bpType byte
timeStart := hrtime.Now()
for {
elapsed := hrtime.Since(timeStart)
if computer.FullSpeed() || int64(elapsed) >= cpuClkPeriod.Load() {
timeStart = hrtime.Now()
bp = 0
bpType = 0
// 2.5MHz frequency
cpuTicks.Add(1)
if !computer.PendingHardReset() && !computer.PendingCpuReset() {
if computer.FullSpeed() {
// Max frequency
_, bp, bpType = computer.Do()
nextTick = cpuTicks.Load()
} else if cpuTicks.Load() >= nextTick {
var t uint32
t, bp, bpType = computer.Do()
nextTick = cpuTicks.Load() + uint64(t)
runtime.Gosched()
}
// Breakpoint hit
if bp > 0 || bpType != 0 {
dezog.BreakpointHit(bp, bpType)
}
} else {
if computer.PendingHardReset() {
computer.HardReset()
} else {
computer.Reset()
}
}
}
}
}
//func initPerf() {
// f, err := os.Create("okemu.prof")
// if err != nil {
// log.Warn("Can not create prof file", err)
// }
// defer func(f *os.File) {
// err := f.Close()
// if err != nil {
// log.Warn("Can not close prof file", err)
// }
// }(f)
// if err := pprof.StartCPUProfile(f); err != nil {
// log.Warn("Can not start CPU profiling", err)
// }
// defer pprof.StopCPUProfile()
//}

View File

@ -1,557 +0,0 @@
package okean240
import (
"context"
_ "embed"
"encoding/binary"
"encoding/json"
"fmt"
"image/color"
"okemu/config"
"okemu/debug"
"okemu/debug/breakpoint"
"okemu/gval"
"okemu/okean240/fdc"
"okemu/okean240/pic"
"okemu/okean240/pit"
"okemu/okean240/usart"
"os"
"strconv"
"sync/atomic"
"fyne.io/fyne/v2"
"github.com/romychs/z80go"
log "github.com/sirupsen/logrus"
)
const DefaultCPUFrequency = 2_500_000
const CPUFrequencyLow float64 = 2.48
const CPUFrequency float64 = 2.50
const CPUFrequencyHi float64 = 2.52
const TimerFrequencyLow float64 = 1.47
const TimerFrequency float64 = 1.50
const TimerFrequencyHi float64 = 1.53
type ComputerType struct {
cpu *z80go.CPU
memory Memory
ioPorts [256]byte
cycles uint64
tstatesPartial uint64
dd17EnableOut bool
colorMode bool
screenWidth int
screenHeight int
vRAM *MemoryBlock
palette byte
bgColor byte
pit *pit.I8253
usart *usart.I8251
pic *pic.I8259
fdc *fdc.FloppyDriveController
kbdBuffer []byte
vShift byte
hShift byte
cpuFrequency uint32
//
debugger *debug.Debugger
config *config.OkEmuConfig
kbAck atomic.Bool
fullSpeed atomic.Bool
pendingCpuReset atomic.Bool
pendingHardReset atomic.Bool
}
type Snapshot struct {
CPU *z80go.CPU `json:"cpu,omitempty"`
Memory string `json:"memory,omitempty"`
}
const VRAMBlock0 = 3
const VRAMBlock1 = 7
const VidVsuBit = 0x80
const VidColorBit = 0x40
//type ComputerInterface interface {
// Run()
// Reset()
// GetPixel(x uint16, y uint16) color.RGBA
// Do() uint64
// TimerClk()
// PutKey(key *fyne.KeyEvent)
// PutRune(key rune)
// PutCtrlKey(shortcut fyne.Shortcut)
// SaveFloppy()
// LoadFloppy()
// CPUState() *z80.CPU
// SetCPUState(state *z80.CPU)
//}
func (c *ComputerType) CPUState() *z80go.CPU {
return c.cpu.GetState()
}
func (c *ComputerType) SetCPUState(state *z80go.CPU) {
c.cpu.SetState(state)
}
func (c *ComputerType) M1MemRead(addr uint16) byte {
return c.memory.M1MemRead(addr)
}
func (c *ComputerType) MemRead(addr uint16) byte {
return c.memory.MemRead(addr)
}
func (c *ComputerType) MemWrite(addr uint16, val byte) {
c.memory.MemWrite(addr, val)
}
// NewComputer Builds new computer
func NewComputer(cfg *config.OkEmuConfig, deb *debug.Debugger) *ComputerType {
c := ComputerType{}
c.config = cfg
c.cpu = z80go.NewCPU(&c)
c.debugger = deb
c.HardReset()
return &c
}
// Reset Only CPU reset.
func (c *ComputerType) Reset() {
// CPU
c.cpu.Reset()
c.cycles = 0
c.tstatesPartial = 0
c.pendingCpuReset.Store(false)
}
// HardReset full computer reset, Use SetPendingHardReset flag to set flag before call this method
func (c *ComputerType) HardReset() {
c.cpu.Reset()
c.memory = Memory{}
c.memory.Init(c.config.MonitorFile, c.config.CPMFile)
c.cycles = 0
c.tstatesPartial = 0
c.dd17EnableOut = false
c.screenWidth = 512
c.screenHeight = 256
c.vRAM = c.memory.allMemory[3]
c.palette = 0
c.bgColor = 0
c.vShift = 0
c.hShift = 0
c.pit = pit.New()
c.kbAck.Store(false)
c.usart = usart.New()
c.pic = pic.NewI8259()
c.fdc = fdc.NewFDC(c.config)
c.cpuFrequency = DefaultCPUFrequency
c.fullSpeed.Store(false)
c.pendingHardReset.Store(false)
c.pendingCpuReset.Store(false)
}
func (c *ComputerType) getContext() map[string]interface{} {
ctx := make(map[string]interface{})
s := c.cpu.GetState()
ctx["A"] = s.A
ctx["B"] = s.B
ctx["C"] = s.C
ctx["D"] = s.D
ctx["E"] = s.E
ctx["H"] = s.H
ctx["L"] = s.L
ctx["A'"] = s.AAlt
ctx["B'"] = s.BAlt
ctx["C'"] = s.CAlt
ctx["D'"] = s.DAlt
ctx["E'"] = s.EAlt
ctx["H'"] = s.HAlt
ctx["L'"] = s.LAlt
ctx["PC"] = s.PC
ctx["SP"] = s.SP
ctx["IX"] = s.IX
ctx["IY"] = s.IY
ctx["ZF"] = s.Flags.Z
ctx["SF"] = s.Flags.S
ctx["NF"] = s.Flags.N
ctx["PF"] = s.Flags.P
ctx["HF"] = s.Flags.H
ctx["YF"] = s.Flags.Y
ctx["XF"] = s.Flags.X
ctx["CF"] = s.Flags.C
ctx["BC"] = uint16(s.B)<<8 | uint16(s.C)
ctx["DE"] = uint16(s.D)<<8 | uint16(s.E)
ctx["HL"] = uint16(s.H)<<8 | uint16(s.L)
ctx["AF"] = uint16(s.A)<<8 | uint16(s.Flags.AsByte())
ctx["MEM"] = c.memory.MemRead
ctx["IO"] = c.IORead
return ctx
}
func (c *ComputerType) Do() (uint32, uint16, byte) {
ticks := uint32(0)
var memAccess *map[uint16]byte
if c.debugger.StepMode() {
if c.debugger.RunMode() || c.debugger.DoStep() {
if c.debugger.RunInst() > 0 {
// skip first instruction after run-mode activated
bpHit, bp := c.debugger.CheckBreakpoints(c.getContext())
if bpHit {
//c.debugger.SetRunMode(false)
return 0, bp, 0
}
}
c.debugger.SaveHistory(c.cpu.GetState())
ticks, memAccess = c.cpu.RunInstruction()
mHit, mAddr, mTyp := c.debugger.CheckMemBreakpoints(memAccess)
if mHit {
return ticks, mAddr, mTyp
}
}
} else {
ticks, memAccess = c.cpu.RunInstruction()
}
c.cycles += uint64(ticks)
c.tstatesPartial += uint64(ticks)
return ticks, 0, 0
}
func (c *ComputerType) GetPixel(x uint16, y uint16) color.RGBA {
if y > 255 {
return CWhite
}
var addr uint16
var resColor color.RGBA
if c.colorMode {
if x > 255 {
return CWhite
}
var offset uint16
if (c.vShift != 0) && (y > 255-uint16(c.vShift)) {
offset = 0x100
} else {
offset = 0
}
y += uint16(c.vShift) & 0x00ff
x += uint16(c.hShift-7) & 0x00ff
// Color 256x256 mode
addr = ((x & 0xf8) << 6) | y
a1 := (addr - offset) & 0x3fff
a2 := (a1 + 0x100) & 0x3fff
cl := (c.vRAM.memory[a1] >> (x & 0x07)) & 1
cl |= ((c.vRAM.memory[a2] >> (x & 0x07)) & 1) << 1
if cl == 0 {
//resColor = BgColorPalette[c.bgColor]
resColor = ColorPalette[c.palette][cl]
} else {
resColor = ColorPalette[c.palette][cl]
}
} else {
if x > 511 {
return CWhite
}
var offset uint16
if (c.vShift != 0) && (y > 255-uint16(c.vShift)) {
offset = 0x100
} else {
offset = 0
}
// Shifts
y += uint16(c.vShift) & 0x00ff
x += uint16(c.hShift-7) & 0x001ff
// Mono 512x256 mode
addr = (((x & 0x1f8) << 5) + y) - offset
pix := c.vRAM.memory[addr&0x3fff] >> (x & 0x07) & 1
if c.palette == 6 {
if pix == 0 {
resColor = CBlack
} else {
resColor = CLGreen
}
} else {
if pix == 0 {
resColor = BgColorPalette[c.bgColor]
} else {
resColor = MonoPalette[c.palette]
}
}
}
return resColor
}
func (c *ComputerType) ScreenWidth() int {
return c.screenWidth
}
func (c *ComputerType) ScreenHeight() int {
return c.screenHeight
}
func (c *ComputerType) Cycles() uint64 {
return c.cycles
}
func (c *ComputerType) ResetTStatesPartial() {
c.tstatesPartial = 0
}
func (c *ComputerType) TStatesPartial() uint64 {
return c.tstatesPartial
}
func (c *ComputerType) TimerClk() {
// DD70 KR580VI53 CLK0, CKL1 @ 1.5MHz
c.pit.Tick(0)
c.pit.Tick(1)
// IRQ from timer
if c.pit.Fired(0) {
c.pic.SetIRQ(RstTimerNo)
}
// clock for SIO KR580VV51
if c.pit.Fired(1) {
c.usart.Tick()
}
}
func (c *ComputerType) SaveFloppy(drive byte) error {
return c.fdc.SaveFloppy(drive)
}
func (c *ComputerType) SetSerialBytes(bytes []byte) {
c.usart.SetRxBytes(bytes)
}
func (c *ComputerType) SetRamBytes(bytes []byte) {
addr := 0x100
for i := 0; i < len(bytes); i++ {
c.memory.MemWrite(uint16(addr), bytes[i])
addr++
}
pages := len(bytes) / 256
if len(bytes)%256 != 0 {
pages++
}
log.Debugf("Loaded bytes: %d; blocks: %d", len(bytes), pages)
//c.cpu.SP = 0x100
//c.cpu.PC = 0x100
}
func (c *ComputerType) Dump(start uint16, length uint16) {
file, err := os.Create("dump.dat")
if err != nil {
log.Error(err)
return
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
log.Error(err)
}
}(file)
var buffer []byte
for addr := start; addr < start+length; addr++ {
buffer = append(buffer, c.memory.MemRead(addr))
}
_, err = file.Write(buffer)
err = binary.Write(file, binary.LittleEndian, buffer)
if err != nil {
log.Error("Save memory dump failed:", err)
} else {
log.Debug("Memory dump saved successfully")
}
}
func (c *ComputerType) CPUFrequency() uint32 {
return c.cpuFrequency
}
func (c *ComputerType) SetCPUFrequency(frequency uint32) {
c.cpuFrequency = frequency
}
func (c *ComputerType) DebuggerState() string {
if c.debugger.StepMode() {
if c.debugger.RunMode() {
return "Run"
}
return "Step"
}
return "Off"
}
func (c *ComputerType) memoryAsHexStr() string {
res := ""
for addr := 0; addr <= 65535; addr++ {
res += fmt.Sprintf("%02X", c.memory.MemRead(uint16(addr)))
}
return res
}
func (c *ComputerType) SaveSnapshot(fn string) error {
// take snapshot
s := Snapshot{
CPU: c.cpu.GetState(),
Memory: c.memoryAsHexStr(),
}
// convert to JSON
b, err := json.Marshal(s)
if err != nil {
return err
}
// and save
err = os.WriteFile(fn, b, 0644)
if err != nil {
return err
}
return nil
}
func (c *ComputerType) LoadSnapshot(fn string) error {
// read snapshot file
b, err := os.ReadFile(fn)
if err != nil {
return err
}
// unmarshal from JSON
var result Snapshot
err = json.Unmarshal(b, &result)
if err != nil {
return err
}
c.cpu.SetState(result.CPU)
return c.restoreMemoryFromHex(result.Memory)
}
func (c *ComputerType) restoreMemoryFromHex(memory string) error {
for addr := 0; addr <= 65535; addr++ {
b, e := strconv.ParseUint(memory[addr*2:addr*2+2], 16, 8)
if e != nil {
log.Error(e)
return e
}
c.memory.MemWrite(uint16(addr), byte(b))
}
return nil
}
func (c *ComputerType) AutoSaveFloppy() {
for drv := byte(0); drv < fdc.TotalDrives; drv++ {
if c.config.FDC[drv].AutoSave {
e := c.fdc.SaveFloppy(drv)
if e != nil {
log.Error(e)
}
}
}
}
func (c *ComputerType) AutoLoadFloppy() {
for drv := byte(0); drv < fdc.TotalDrives; drv++ {
if c.config.FDC[drv].AutoLoad {
e := c.fdc.LoadFloppyFile(drv, c.config.FDC[drv].FloppyFile)
if e != nil {
log.Error(e)
}
}
}
}
func (c *ComputerType) ClearCodeCoverage() {
c.cpu.ClearCodeCoverage()
}
func (c *ComputerType) SetCodeCoverage(enabled bool) {
c.cpu.SetCodeCoverage(enabled)
}
func (c *ComputerType) CodeCoverage() map[uint16]bool {
return c.cpu.CodeCoverage()
}
func (c *ComputerType) SetExtendedStack(enabled bool) {
c.cpu.SetExtendedStack(enabled)
}
func (c *ComputerType) ExtendedStack() (map[uint16]z80go.PushValueType, error) {
return c.cpu.ExtendedStack()
}
var language gval.Language
func init() {
language = gval.NewLanguage(gval.Base(), gval.Arithmetic(), gval.Bitmask(), gval.PropositionalLogic(),
gval.Function("PEEKW", breakpoint.CfPeekW),
gval.Function("PEEK", breakpoint.CfPeek),
gval.Function("BYTE", breakpoint.CfByte),
gval.Function("WORD", breakpoint.CfWord),
gval.Function("ABS", breakpoint.CfAbs),
gval.Function("IN", breakpoint.CfIn),
)
}
func (c *ComputerType) Evaluate(expression string) (string, error) {
params := c.getContext()
bc := context.WithValue(context.Background(), "MEM", params["MEM"])
bc = context.WithValue(bc, "IO", params["IO"])
eval, err := language.NewEvaluable(expression)
if err != nil {
return "", fmt.Errorf("error: %s", err.Error())
}
value, err := eval.EvalUint(bc, params)
if err != nil {
return "", fmt.Errorf("error: %s", err.Error())
}
return strconv.FormatUint(uint64(value), 10), nil
}
func (c *ComputerType) MemoryPages() []byte {
return c.memory.MemoryWindows()
}
func (c *ComputerType) SetFullSpeed(full bool) {
c.fullSpeed.Store(full)
}
func (c *ComputerType) FullSpeed() bool {
return c.fullSpeed.Load()
}
func (c *ComputerType) SetPendingHardReset(pending bool) {
c.pendingHardReset.Store(pending)
}
func (c *ComputerType) PendingHardReset() bool {
return c.pendingHardReset.Load()
}
func (c *ComputerType) SetPendingCpuReset(pending bool) {
c.pendingCpuReset.Store(pending)
}
func (c *ComputerType) PendingCpuReset() bool {
return c.pendingCpuReset.Load()
}
func (c *ComputerType) LoadFloppyData(drive byte, reader fyne.URIReadCloser) error {
return c.fdc.LoadFloppyData(drive, reader)
}

View File

@ -1,20 +0,0 @@
package okean240
func calculateVAddress(x uint16, y uint16) (uint16, uint16) {
var offset uint16
if (c.vShift != 0) && (y > 255-uint16(c.vShift)) {
offset = 0x100
} else {
offset = 0
}
y += uint16(c.vShift) & 0x00ff
x += uint16(c.hShift-7) & 0x00ff
// Color 256x256 mode
addr = ((x & 0xf8) << 6) | y
a1 := (addr - offset) & 0x3fff
a2 := (addr + 0x100 - offset) & 0x3fff
}

View File

@ -1,157 +0,0 @@
package okean240
/*
* КР580ВВ55 DD79 USER PORT
*/
// USR_DD79PA User port A
//const USR_DD79PA = 0x00
// USR_DD79PB User port B
//const USR_DD79PB = 0x01
// USR_DD79PC User port C
//const USR_DD79PC = 0x02
// USR_DD79CTR Config
// Configure: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
// Set bit: [0][xxx][bbb][0|1]
//const USR_DD79CTR = 0x03
/*
* КР1818ВГ93 FDC Controller
*/
// FdcCmd FDC Command
const FdcCmd = 0x20
// FdcTrack FDC Track No
const FdcTrack = 0x21
// FdcSect FDC Sector
const FdcSect = 0x22
// FdcData FDC Data
const FdcData = 0x23
// FdcDrq Read DRQ state from FDC
const FdcDrq = 0x24
/*
* Floppy Controller port
*/
// Floppy Controller port
// WR: 5-SSEN, 4-#DDEN, 3-INIT, 2-DRSEL, 1-MOT1, 0-MOT0
// RD: 7-MOTST, 6-SSEL, 5,4-x , 3-DRSEL, 2-MOT1, 1-MOT0, 0-INT
const Floppy = 0x25
/*
* КР580ВВ55 DD78 Keyboard
*/
// KbdDd78pa Port A - Keyboard Data
const KbdDd78pa = 0x40
// KbdDd78pb Port B - JST3,SHFT,CTRL,ACK,TAPE5,TAPE4,GK,GC
const KbdDd78pb = 0x41
// KBD_DD78PC Port C - [PC7:5],[KBD_ACK],[PC3:0]
const KbdDd78pc = 0x42
// KBD_DD78CTR Control port
// Configure: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
// Set bit: [0][xxx][bbb][0|1]
// const KBD_DD78CTR = 0x43
/*
* КР580ВИ53 DD70
*/
// TmrDd70c1 Timer load 1
const TmrDd70c1 = 0x60
// TmrDd70c2 Timer load 2
const TmrDd70c2 = 0x61
// TmrDd70c3 Timer load 3
const TmrDd70c3 = 0x62
// TmrDd70ctr
// Timer config: [sc1,sc0][rl1,rl0][m2,m1,m0][bcd]
//
// sc - timer, rl=01-LSB, 10-MSB, 11-LSB+MSB
// mode 000 - intRq on fin,
// 001 - one shot,
// x10 - rate gen,
// x11-sq wave
const TmrDd70ctr = 0x63
/*
* Programmable Interrupt controller PIC КР580ВН59
*/
const RstKbdNo = 1
const RstTimerNo = 4
const KbAckBit = 0x80
// const Rst0Mask = 0x01 // System interrupt
const RstKbdMask = 0x02 // Keyboard interrupt
// const Rst2Mask = 0x04 // Serial interface interrupt
// const Rst3Mask = 0x08 // Printer ready
const RstTmrMask = 0x10
//const Rst5Mask = 0x20 // Power intRq
//const Rst6Mask = 0x40 // User device 1 interrupt
//const Rst7Mask = 0x80 // User device 1 interrupt
// PicDd75a Port A (a0=0)
const PicDd75a = 0x80
// PicDd75b Port B (a0=1)
const PicDd75b = 0x81
/*
* КР580ВВ51 DD72
*/
// UartDd72rd Serial data
const UartDd72rd = 0xA0
// UartDd72rr Serial enabled [RST,RQ_RX,RST_ERR,PAUSE,RX_EN,RX_RDY,TX_RDY]
const UartDd72rr = 0xA1
/*
* КР580ВВ55 DD17 System port
*/
// SysDd17pa Port A - VShift[8..1] Vertical shift
const SysDd17pa = 0xC0
// SysDd17pb Port B - Memory mapper [ROM14,13][REST][ENROM-][A18,17,16][32k]
const SysDd17pb = 0xC1
// SysDd17pc Port C - HShift[HS5..1,SB3..1] Horisontal shift
const SysDd17pc = 0xC2
// SysDd17ctr Configure: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
// Set bit: [0][xxx][bbb][0|1]
const SysDd17ctr = 0xC3
/*
* КР580ВВ55 DD67
*/
// LPT_DD67PA Port A - Printer Data
//const LPT_DD67PA = 0xE0
// VidDd67pb Port B - Video control [VSU,C/M,FL3:1,COL3:1]
const VidDd67pb = 0xE1
// DD67PC Port C - [USER3:1, STB-LP, BELL, TAPE3:1]
//const DD67PC = 0xE2
// DD67CTR
// Configure: [1][ma1,ma0][0-aO|1-aI],[0-chO,1-chI],[mb],[0-bO|1-bI],[0-clO,1-clI]
// Set bit: [0][xxx][bbb][0|1]
// const DD67CTR = 0xE3

View File

@ -1,470 +0,0 @@
package fdc
/**
* Floppy drive controller, based on
* MB8877, К1818ВГ93
*
* By Romych, 2025.03.05
*/
import (
"bytes"
"encoding/binary"
"errors"
"io"
"okemu/config"
"os"
"slices"
"strconv"
"github.com/howeyc/crc16"
log "github.com/sirupsen/logrus"
)
// Floppy parameters
const (
FloppyB = 0
FloppyC = 1
TotalDrives = 2
FloppySizeK = 720
SectorSize = 512
SideCount = 2
SectorPerTrack = 9
SizeInSectors = FloppySizeK * 1024 / SectorSize
TracksCount = SizeInSectors / SideCount / SectorPerTrack
SectorsPerSide = SizeInSectors / SideCount
TrackHeaderSize = 146
TrackSectorSize = 626
TrackFooterSize = 256 * 3
TrackBufferSize = TrackHeaderSize + TrackSectorSize*SectorPerTrack + TrackFooterSize
)
// FDC Commands
const (
CmdRestore = 0x0
CmdSeek = 0x1
CmdStep = 0x2
CmdStepIn = 0x5
CmdStepOut = 0x7
CmdReadSector = 0x8
CmdReadSectorMulti = 0x9
CmdWriteSector = 0xa
CmdReadAddress = 0xc
CmdWriteTrack = 0xf
CmdNoCommand = 0xff
)
var interleave = []byte{1, 8, 6, 4, 2, 9, 7, 5, 3}
const (
StatusTR0 = 0x04 // TR0 - Head at track 0
StatusRNF = 0x10 // RNF - Record not found
// StatusSeekError = 0x10 // Sector out of disk
StatusHeadLoaded = 0x20 // Head on disk
)
type SectorType []byte
type FloppyDriveController struct {
// Floppy controller port
sideNo byte
ddEn byte
init byte
drive byte
mot1 byte
mot0 byte
intRq byte
motSt byte
sectorNo byte
trackNo byte
drq byte
// FloppyStorage B and C
sectors [TotalDrives][SizeInSectors]SectorType
data byte
status byte
lastCmd byte
//curSector *SectorType
bytePtr uint16
trackBuffer []byte
// floppyFile []string
config *config.OkEmuConfig
}
type FloppyDriveControllerInterface interface {
SetFloppy()
Floppy() byte
SetCmd(value byte)
Status() byte
SetTrack(value byte)
SetSector(value byte)
SetData(value byte)
Data() byte
Drq() byte
SaveFloppy(drive byte)
GetSectorNo() uint16
Track() byte
Sector() byte
}
//var slicer = []uint16{1, 8, 6, 4, 2, 9, 7, 5, 3}
func getSectorNo(side byte, track byte, sector byte) uint16 {
//return (uint16(track)*18 + uint16(side)*9 + slicer[sector-1] - 1)
return uint16(side)*SectorsPerSide + uint16(track)*SectorPerTrack + uint16(sector) - 1
}
func (f *FloppyDriveController) GetSectorNo() uint16 {
return getSectorNo(f.sideNo, f.trackNo, f.sectorNo)
}
func (f *FloppyDriveController) SetFloppy(val byte) {
// WR: 5-SSEL, 4-#DDEN, 3-INIT, 2-DRSEL, 1-MOT1, 0-MOT0
f.sideNo = val >> 5 & 0x01
f.ddEn = val >> 4 & 0x01
f.init = val >> 3 & 0x01
f.drive = (^val) >> 2 & 0x01
f.mot1 = val >> 1 & 0x01
f.mot0 = val & 0x01
}
func (f *FloppyDriveController) GetFloppy() byte {
// RD: 7-MOTST, 6-SSEL, 5,4-x , 3-DRSEL, 2-MOT1, 1-MOT0, 0-INT
floppy := f.intRq | (f.mot0 << 1) | (f.mot1 << 2) | ((^f.drive & 1) << 3) | (f.sideNo << 6) | (f.motSt << 7)
return floppy
}
var crcTable *crc16.Table
func init() {
crcTable = crc16.MakeTable(0xffff)
}
func (f *FloppyDriveController) SetCmd(value byte) {
f.lastCmd = value >> 4
switch f.lastCmd {
case CmdRestore:
log.Trace("CMD Restore (seek trackNo 0)")
f.trackNo = 0
f.status = StatusTR0 | StatusHeadLoaded // TR0 & Head loaded
case CmdSeek:
log.Tracef("CMD Seek %x", value&0xf)
f.status = StatusHeadLoaded
f.trackNo = f.data
case CmdStep:
log.Tracef("CMD Step %x", value&0xf)
f.status = StatusHeadLoaded
f.trackNo = f.data
case CmdStepIn:
log.Tracef("CMD StepIn (Next track) %x", value&0xf)
f.status = StatusHeadLoaded
if f.trackNo < TracksCount {
f.trackNo++
}
case CmdStepOut:
log.Tracef("CMD StepOut (Previous track) %x", value&0xf)
f.status = StatusHeadLoaded
if f.trackNo > 0 {
f.trackNo--
}
case CmdReadSector:
sectorNo := f.GetSectorNo()
log.Tracef("CMD Read single sectorNo: %d", sectorNo)
if sectorNo < SizeInSectors {
f.trackBuffer = slices.Clone(f.sectors[f.drive][sectorNo])
f.drq = 1
f.status = 0x00
} else {
f.drq = 0
f.status = StatusRNF
}
case CmdReadSectorMulti:
sectorNo := f.GetSectorNo()
f.trackBuffer = []byte{}
for c := 0; c < SectorPerTrack; c++ {
f.trackBuffer = slices.Concat(f.trackBuffer, f.sectors[f.drive][sectorNo])
sectorNo++
}
f.drq = 1
f.status = 0x0
case CmdWriteSector:
sectorNo := f.GetSectorNo()
log.Tracef("CMD Write Sector %d", sectorNo)
if sectorNo < SizeInSectors {
f.bytePtr = 0
f.drq = 1
f.status = 0x0
f.trackBuffer = []byte{}
} else {
f.drq = 0
f.status = StatusRNF
}
case CmdReadAddress:
log.Tracef("CMD ReadAddress %d", value)
f.trackBuffer = []byte{f.trackNo, f.sideNo, f.sectorNo, 2}
checksum := crc16.Checksum(f.trackBuffer, crcTable)
f.trackBuffer = append(f.trackBuffer, byte(checksum))
f.trackBuffer = append(f.trackBuffer, byte(checksum>>8))
f.drq = 1
f.status = 0x0
case CmdWriteTrack:
log.Tracef("CMD Write Track %x", f.trackNo)
f.drq = 1
f.status = 0x00
f.trackBuffer = []byte{}
default:
log.Errorf("Unknown CMD: %x VAL: %x", f.lastCmd, value&0xf)
}
}
func (f *FloppyDriveController) Status() byte {
return f.status
}
func (f *FloppyDriveController) SetTrackNo(value byte) {
//log.Tracef("FDC Track: %d", value)
if value > TracksCount {
f.status |= 0x10 /// RNF
log.Errorf("Track %d not found!", value)
} else {
f.trackNo = value
}
}
func (f *FloppyDriveController) SetSectorNo(value byte) {
//log.Tracef("FDC Sector: %d", value)
if value > SectorPerTrack {
f.status |= 0x10
log.Errorf("Record not found %d!", value)
} else {
f.sectorNo = value
}
}
func (f *FloppyDriveController) SetData(value byte) {
//log.Tracef("FCD Data: %d", value)
if f.lastCmd == CmdWriteTrack {
if len(f.trackBuffer) < TrackBufferSize {
f.trackBuffer = append(f.trackBuffer, value)
f.drq = 1
f.status = 0x00
} else {
//f.dump()
f.writeTrack()
f.drq = 0
f.status = 0x00
f.lastCmd = CmdNoCommand
}
} else if f.lastCmd == CmdWriteSector {
if len(f.trackBuffer) < SectorSize {
f.trackBuffer = append(f.trackBuffer, value)
if len(f.trackBuffer) == SectorSize {
f.drq = 0
} else {
f.drq = 1
}
}
if len(f.trackBuffer) == SectorSize {
f.drq = 0
f.sectors[f.drive][f.GetSectorNo()] = slices.Clone(f.trackBuffer)
f.lastCmd = CmdNoCommand
}
}
f.data = value
}
const SectorInfoSize = 626
const SectorInfoOffset = 0x0092
const TrackNoOffset = 0x0010
const SideNoOffset = 0x0011
const SectorNoOffset = 0x0012
const SectorLengthOffset = 0x0013
const SectorDataOffset = 0x003b
var SectorLengths = []int{128, 256, 512, 1024}
func (f *FloppyDriveController) writeTrack() {
// skip header
ptr := SectorInfoOffset
// repeat for every sector on track
for sec := 0; sec < SectorPerTrack; sec++ {
// get info from header
trackNo := f.trackBuffer[ptr+TrackNoOffset]
sideNo := f.trackBuffer[ptr+SideNoOffset]
sectorNo := f.trackBuffer[ptr+SectorNoOffset]
sectorLength := SectorLengths[f.trackBuffer[ptr+SectorLengthOffset]&0x03]
// get sector data
sectorData := f.trackBuffer[ptr+SectorDataOffset : ptr+SectorDataOffset+sectorLength]
absSector := getSectorNo(sideNo, trackNo, sectorNo)
log.Debugf("Write Drive: %d; side:%d; T: %d S: %d Len: %d Data: [%X..%X]; Abs sector: %d", f.drive, sideNo, trackNo, sectorNo, len(sectorData), sectorData[0], sectorData[len(sectorData)-1], absSector)
// write data to sector buffer
f.sectors[f.drive][absSector] = slices.Clone(sectorData)
// shift pointer to next sector info block
ptr += SectorInfoSize
}
}
func (f *FloppyDriveController) Data() byte {
switch f.lastCmd {
case CmdReadSector, CmdReadSectorMulti, CmdReadAddress:
if len(f.trackBuffer) > 0 {
f.drq = 1
f.data = f.trackBuffer[0]
f.trackBuffer = f.trackBuffer[1:]
}
if len(f.trackBuffer) == 0 {
f.drq = 0
f.status = 0
f.lastCmd = CmdNoCommand
}
default:
f.data = 0xff
}
return f.data
}
func (f *FloppyDriveController) Drq() byte {
return f.drq
}
//func (f *FloppyDriveController) LoadFloppy(drive byte) error {
// if drive < TotalDrives {
// return f.LoadConfiguredFloppy(drive)
// }
// return errors.New("DriveNo " + strconv.Itoa(int(drive)) + " out of range")
//}
func (f *FloppyDriveController) SaveFloppy(drive byte) error {
if drive < TotalDrives {
return saveFloppy(&f.sectors[drive], f.config.FDC[drive].FloppyFile)
}
return errors.New("DriveNo " + strconv.Itoa(int(drive)) + " out of range")
}
func NewFDC(conf *config.OkEmuConfig) *FloppyDriveController {
sec := [2][SizeInSectors]SectorType{}
// for each drive
for d := 0; d < TotalDrives; d++ {
// for each sector
for i := 0; i < SizeInSectors; i++ {
sec[d][i] = bytes.Repeat([]byte{0xe5}, SectorSize)
}
}
return &FloppyDriveController{
sideNo: 0,
ddEn: 0,
init: 0,
drive: 0,
mot1: 0,
mot0: 0,
intRq: 0,
motSt: 0,
drq: 0,
lastCmd: 0xff,
sectors: sec,
bytePtr: 0xffff,
//floppyConf: conf.FDC,
config: conf,
}
}
func (f *FloppyDriveController) dump() {
log.Trace("Dump Buffer content.")
file, err := os.Create("track-" + strconv.Itoa(int(f.trackNo)) + ".dat")
if err != nil {
log.Error(err)
return
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
log.Error(err)
}
}(file)
err = binary.Write(file, binary.LittleEndian, f.trackBuffer)
if err != nil {
log.Error("Save track content failed:", err)
}
}
func (f *FloppyDriveController) Track() byte {
return f.trackNo
}
func (f *FloppyDriveController) Sector() byte {
return f.sectorNo
}
func (f *FloppyDriveController) LoadFloppyData(drive byte, reader io.ReadCloser) error {
if drive >= TotalDrives {
return errors.New("Drive " + strconv.Itoa(int(drive+65)) + " out of range")
}
var err error
for sector := 0; sector < SizeInSectors; sector++ {
var n int
n, err = reader.Read(f.sectors[drive][sector])
if n != SectorSize {
log.Error("Load floppy error, sector size: %d <> %d", n, SectorSize)
}
if err != nil {
log.Error("Load floppy content failed:", err)
return err
}
}
return nil
}
// LoadFloppyFile load floppy image to sector buffer from file
func (f *FloppyDriveController) LoadFloppyFile(drive byte, fileName string) error {
//fileName := f.config.FDC[drive].FloppyFile
log.Debugf("Load Floppy content from file %s.", fileName)
file, err := os.Open(fileName)
if err != nil {
log.Error(err)
return err
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
log.Error(err)
}
}(file)
return f.LoadFloppyData(drive, file)
}
// saveFloppy Save specified sectors to file with name fileName
func saveFloppy(sectors *[SizeInSectors]SectorType, fileName string) error {
log.Debugf("Save Floppy to file %s.", fileName)
file, err := os.Create(fileName)
if err != nil {
log.Error(err)
return err
}
defer func(file *os.File) {
err := file.Close()
if err != nil {
log.Error(err)
}
}(file)
for sector := 0; sector < SizeInSectors; sector++ {
var n int
n, err = file.Write(sectors[sector])
if n != SectorSize {
log.Errorf("Save floppy error, sector %d size: %d <> %d", sector, n, SectorSize)
}
if err != nil {
log.Error("Save floppy content failed:", err)
return err
}
}
return nil
}

View File

@ -1,133 +0,0 @@
package okean240
/*
* Ocean-240.2
* Computer with FDC variant.
* IO Ports definitions
*
* By Romych 2026-03-01
*/
import log "github.com/sirupsen/logrus"
func (c *ComputerType) IORead(port uint16) uint8 {
switch port & 0x00ff {
case PicDd75a:
// PIO xx59, get IRR register
irr := c.pic.IRR()
// if irq from keyboard and no ACK applied, re-fire
if irr&RstKbdMask != 0 && !c.kbAck.Load() {
log.Tracef("KBD IRQ REFIRE PC=%04X", c.cpu.PC)
c.pic.SetIRQ(RstKbdNo)
}
return irr
case PicDd75b:
return c.pic.CSW()
case UartDd72rr:
// USART VV51 CMD
return c.usart.Status()
case UartDd72rd:
// USART VV51 Data
return c.usart.Receive()
case KbdDd78pa:
// Keyboard data
log.Tracef("KBD RD: %d, PC=%04X", c.ioPorts[KbdDd78pa], c.cpu.PC)
return c.ioPorts[KbdDd78pa]
case KbdDd78pb:
return c.ioPorts[KbdDd78pb]
case FdcCmd:
return c.fdc.Status()
case FdcDrq:
return c.fdc.Drq()
case Floppy:
return c.fdc.GetFloppy()
case FdcData:
return c.fdc.Data()
case FdcTrack:
return c.fdc.Track()
case FdcSect:
return c.fdc.Sector()
default:
log.Debugf("IORead from port: %x", port)
}
return c.ioPorts[byte(port&0x00ff)]
}
func (c *ComputerType) IOWrite(port uint16, val byte) {
bp := byte(port & 0x00ff)
c.ioPorts[bp] = val
//log.Debugf("OUT (%x), %x", bp, val)
switch bp {
case SysDd17pb:
if c.dd17EnableOut {
c.memory.Configure(val)
}
case SysDd17ctr:
c.dd17EnableOut = val == 0x80
case VidDd67pb:
if val&VidVsuBit == 0 {
// video page 0
c.vRAM = c.memory.allMemory[VRAMBlock0]
} else {
// video page 1
c.vRAM = c.memory.allMemory[VRAMBlock1]
}
if val&VidColorBit != 0 {
c.colorMode = true
c.screenWidth = 256
} else {
c.colorMode = false
c.screenWidth = 512
}
c.palette = val & 0x07
c.bgColor = (val >> 3) & 0x07
case SysDd17pa:
c.vShift = val
case SysDd17pc:
c.hShift = val
case TmrDd70ctr:
// Timer VI63 config register
c.pit.Configure(val)
case TmrDd70c1:
// Timer VI63 counter0 register
c.pit.Load(0, val)
case TmrDd70c2:
// Timer VI63 counter1 register
c.pit.Load(1, val)
case TmrDd70c3:
// Timer VI63 counter2 register
c.pit.Load(2, val)
case UartDd72rr:
// USART VV51 CMD
c.usart.Command(val)
case UartDd72rd:
// USART VV51 Data
c.usart.Send(val)
case PicDd75b:
c.pic.SetCSW(val)
case FdcCmd:
c.fdc.SetCmd(val)
case FdcData:
c.fdc.SetData(val)
case FdcTrack:
c.fdc.SetTrackNo(val)
case FdcSect:
c.fdc.SetSectorNo(val)
case Floppy:
c.fdc.SetFloppy(val)
case KbdDd78pc:
if val&KbAckBit != 0 {
c.kbAck.Store(true)
log.Trace("KBD ACK")
} else {
if c.kbAck.Load() {
c.pic.ResetIRQ(RstKbdNo)
}
}
default:
//log.Debugf("OUT to Unknown port (%x), %x", bp, val)
}
}

View File

@ -1,46 +0,0 @@
package okean240
import (
"fyne.io/fyne/v2"
log "github.com/sirupsen/logrus"
)
func (c *ComputerType) PutKey(key *fyne.KeyEvent) {
code := RemapCmdKey[key.Name]
if code > 0 {
log.Tracef("PutKey keyName: %s", key.Name)
c.ioPorts[KbdDd78pa] = code
c.kbAck.Store(false)
c.pic.SetIRQ(RstKbdNo)
}
}
func (c *ComputerType) PutRune(key rune) {
log.Tracef("Put Rune: %c Lo: %x, Hi: %x", key, key&0xff, key>>8)
c.ioPorts[KbdDd78pa] = byte(key & 0xff)
c.kbAck.Store(false)
c.pic.SetIRQ(RstKbdNo)
}
/*
CTRL_C EQU 0x03 ; Warm boot
CTRL_H EQU 0x08 ; Backspace
CTRL_E EQU 0x05 ; Move to beginning of new line (Physical EOL)
CTRL_J EQU 0x0A ; LF - Line Feed
CTRL_M EQU 0x0D ; CR - Carriage Return
CTRL_P EQU 0x10 ; turn on/off printer
CTRL_R EQU 0x12 ; Repeat current cmd line
CTRL_S EQU 0x13 ; Temporary stop display data to console (aka DC3)
CTRL_U EQU 0x15 ; Cancel (erase) current cmd line
CTRL_X EQU 0x18 ; Cancel (erase) current cmd line
*/
func (c *ComputerType) PutCtrlKey(key byte) {
c.ioPorts[KbdDd78pa] = key
c.pic.SetIRQ(RstKbdNo)
c.kbAck.Store(false)
c.ioPorts[KbdDd78pb] &= 0x1f | 0x20
}

View File

@ -1,51 +0,0 @@
package okean240
import "image/color"
var CRed = color.RGBA{R: 0xff, G: 0, B: 0, A: 0xff}
var CLGreen = color.RGBA{R: 0x00, G: 0xff, B: 0, A: 0xff}
var CGreen = color.RGBA{R: 0x12, G: 0x76, B: 0x22, A: 0xff}
var CBlue = color.RGBA{R: 0x2A, G: 0x60, B: 0x99, A: 0xff}
var CLBlue = color.RGBA{R: 0x72, G: 0x9F, B: 0xCF, A: 0xff}
var CCrimson = color.RGBA{R: 0xbf, G: 0x00, B: 0xbf, A: 0xff}
var CWhite = color.RGBA{R: 0xee, G: 0xee, B: 0xee, A: 0xff}
var CYellow = color.RGBA{R: 0xff, G: 0xff, B: 0x00, A: 0xff}
var CBlack = color.RGBA{R: 0, G: 0, B: 0, A: 0xff}
//// R5 - Style Palette
//var ColorPalette = [8][4]color.RGBA{
// {CBlack, CRed, CGreen, CBlue}, // 000
// {CWhite, CRed, CGreen, CBlue}, // 001
// {CRed, CGreen, CLBlue, CYellow}, // 010
// {CBlack, CRed, CCrimson, CWhite}, // 011
// {CBlack, CRed, CWhite, CBlue}, // 100
// {CBlack, CRed, CWhite, CYellow}, // 101
// {CGreen, CWhite, CYellow, CBlue}, // 110
// {CBlack, CBlack, CBlack, CBlack}, // 111
//}
// ColorPalette R8 style palette
var ColorPalette = [8][4]color.RGBA{
{CBlack, CGreen, CBlue, CRed}, // 000
{CWhite, CGreen, CBlue, CRed}, // 001
{CGreen, CBlue, CCrimson, CLBlue}, // 010
{CBlack, CGreen, CYellow, CWhite}, // 011
{CBlack, CGreen, CLBlue, CRed}, // 100
{CBlack, CRed, CBlue, CLBlue}, // 101
{CBlue, CWhite, CLBlue, CRed}, // 110
{CBlack, CBlack, CBlack, CBlack}, // 111
}
// BgColorPalette Background color palette
var BgColorPalette = [8]color.RGBA{
CBlack, CBlue, CGreen, CLBlue, CRed, CCrimson /*CYellow*/, CBlack, CWhite,
}
var MonoPalette = [8]color.RGBA{
CWhite, CRed, CGreen, CBlue, CLBlue, CYellow, CLGreen, CBlack,
}
//// MonoPalette R8 Style monochrome mode palette
//var MonoPalette = [8]color.RGBA{
// CRed, CBlue, CCrimson, CGreen, CYellow, CLBlue, CWhite, CBlack,
//}

View File

@ -1,78 +0,0 @@
package pic
import log "github.com/sirupsen/logrus"
/*
Programmable Interrupt Controller
i8058, MSM82C59, КР580ВН59
By Romych, 2025.03.05
*/
type I8259 struct {
irr byte
csw byte
}
type I8259Interface interface {
SetIRQ(irq byte)
IRR() byte
CSW() byte
SetCSW(val byte)
}
// IRR Return value of interrupt request register
func (c *I8259) IRR() byte {
irr := c.irr
// Reset the highest IR bit
if irr&0x01 != 0 {
c.irr &= 0xFE
} else if irr&0x02 != 0 {
c.irr &= 0xFD
} else if irr&0x04 != 0 {
c.irr &= 0xFB
} else if irr&0x08 != 0 {
c.irr &= 0xF7
} else if irr&0x10 != 0 {
c.irr &= 0xEF
} else if irr&0x20 != 0 {
c.irr &= 0xDF
} else if irr&0x40 != 0 {
c.irr &= 0xBF
} else if irr&0x80 != 0 {
c.irr &= 0x7F
}
return irr
}
// ResetIRQ Reset interrupt request flag for specified irq
func (c *I8259) ResetIRQ(irq byte) {
c.irr &= ^(byte(1) << (irq & 0x07))
log.Tracef("RESET IRQ %d -> IRR: %08b", irq, c.irr)
}
// SetIRQ Set interrupt request flag for specified irq
func (c *I8259) SetIRQ(irq byte) {
c.irr |= 1 << (irq & 0x07)
if irq == 1 {
log.Tracef("SET IRQ %d -> IRR: %08b", irq, c.irr)
}
}
// CSW Return value of CSW register
func (c *I8259) CSW() byte {
return c.csw
}
// SetCSW Set value of CSW register
func (c *I8259) SetCSW(val byte) {
c.csw = val
}
// NewI8259 Create and initialize new i8259 controller
func NewI8259() *I8259 {
return &I8259{
irr: 0,
csw: 0,
}
}

View File

@ -1,158 +0,0 @@
package pit
/*
Programmable Interval Timer
i8053, MSM82C53, КР580ВИ53
By Romych, 2025.03.04
*/
// Timer work modes
const (
TimerModeIntOnFin = iota
TimerModeOneShot
TimerModeRateGen
TimerModeSqWave
)
// Timer load counter modes
const (
TimerRLMsbLsb = iota
TimerRLLsb
TimerRLMsb
TimerRLLsbMsb
)
type Timer8253Ch struct {
rl byte // load mode
mode byte // counter mode
bcd bool // decimal/BCD load mode
load uint16 // value to count from
counter uint16 // timer counter
fb bool // first byte load flag
started bool // true if timer started
fired bool
}
type I8253 struct {
//chNo byte
channel [3]Timer8253Ch
}
type I8253Interface interface {
Configure(value byte)
Load(chNo int, value byte)
Counter(chNo int) uint16
Fired(chNo int) bool
Start(chNo int) bool
}
func New() *I8253 {
return &I8253{
//chNo: 0,
channel: [3]Timer8253Ch{
{0, 0, false, 0, 0, true, false, false},
{0, 0, false, 0, 0, true, false, false},
{0, 0, false, 0, 0, true, false, false},
},
}
}
func (t *I8253) Tick(chNo int) {
tmr := &t.channel[chNo]
if tmr.started {
tmr.counter--
if tmr.counter == 0 {
switch tmr.mode {
case TimerModeIntOnFin:
{
tmr.started = false
tmr.fired = true
}
case TimerModeOneShot:
tmr.started = false
case TimerModeRateGen:
tmr.started = false
case TimerModeSqWave:
{
tmr.started = true
tmr.counter = tmr.load
tmr.fired = true
}
}
}
}
}
func (t *I8253) Counter(chNo int) uint16 {
return t.channel[chNo].counter
}
func (t *I8253) Fired(chNo int) bool {
f := t.channel[chNo].fired
if f {
t.channel[chNo].fired = false
}
return f
}
func (t *I8253) Start(chNo int) bool {
return t.channel[chNo].started
}
/*
Timer config byte: [sc1:0][rl1:0][m2:0][bcd]
sc1:0 - timer No
rl=01-LSB, 10-MSB, 11-LSB+MSB
mode 000 - intRq on fin,
001 - one shot,
x10 - rate gen,
x11 - sq wave
*/
func (t *I8253) Configure(value byte) {
chNo := (value & 0xC0) >> 6
rl := value & 0x30 >> 4
t.channel[chNo].started = false
t.channel[chNo].rl = rl
t.channel[chNo].mode = (value & 0x0E) >> 1
t.channel[chNo].fb = true
t.channel[chNo].bcd = value&0x01 == 1
t.channel[chNo].load = 0
}
func (t *I8253) Load(chNo byte, value byte) {
timer := &t.channel[chNo]
switch timer.rl {
case TimerRLMsbLsb:
// MSB+LSB
if timer.fb {
// MSB
timer.load = uint16(value) << 8
timer.fb = false
} else {
// LSB
timer.load |= uint16(value)
timer.started = true
}
case TimerRLLsb:
// LSB Only
timer.load = (timer.load & 0xff00) | uint16(value)
timer.started = true
case TimerRLMsb:
// MSB Only
timer.load = (timer.load & 0x00ff) | (uint16(value) << 8)
timer.started = true
case TimerRLLsbMsb:
// LSB+MSB
if timer.fb {
// LSB
timer.load = uint16(value)
timer.fb = false
} else {
// MSB
timer.load = (uint16(value) << 8) | (timer.load & 0x00ff)
timer.started = true
timer.counter = timer.load
}
}
}

View File

@ -1,196 +0,0 @@
package okean240
import "fyne.io/fyne/v2"
var RemapCmdKey = map[fyne.KeyName]byte{
fyne.KeyEscape: 0x1B,
fyne.KeyReturn: 0x0D,
fyne.KeyTab: 0x09,
fyne.KeyBackspace: 0x08,
fyne.KeyInsert: 0x00,
fyne.KeyDelete: 0x08,
fyne.KeyRight: 0x84, //0x18,
fyne.KeyLeft: 0x93, //0x08,
fyne.KeyDown: 0x0A,
fyne.KeyUp: 0x85, //0x19,
fyne.KeyPageUp: 0x00,
fyne.KeyPageDown: 0x00,
fyne.KeyHome: 0x0C,
fyne.KeyEnd: 0x1A,
fyne.KeyF1: 0x00,
fyne.KeyF2: 0x00,
fyne.KeyF3: 0x00,
fyne.KeyF4: 0x00,
fyne.KeyF5: 0x00,
fyne.KeyF6: 0x00,
fyne.KeyF7: 0x00,
fyne.KeyF8: 0x00,
fyne.KeyF9: 0x00,
fyne.KeyF10: 0x00,
fyne.KeyF11: 0x00,
fyne.KeyF12: 0x00,
fyne.KeyEnter: 0x0A,
fyne.KeyUnknown: 0x00,
}
//var RemapKey = map[fyne.KeyName]byte{
// fyne.KeyEscape: 0x1B,
// fyne.KeyReturn: 0x0A,
// fyne.KeyTab: 0x09,
// fyne.KeyBackspace: 0x08,
// fyne.KeyInsert: 0x00,
// fyne.KeyDelete: 0x08,
// fyne.KeyRight: 0x18,
// fyne.KeyLeft: 0x08,
// fyne.KeyDown: 0x0A,
// fyne.KeyUp: 0x19,
// fyne.KeyPageUp: 0x00,
// fyne.KeyPageDown: 0x00,
// fyne.KeyHome: 0x0C,
// fyne.KeyEnd: 0x1A,
// fyne.KeyF1: 0x00,
// fyne.KeyF2: 0x00,
// fyne.KeyF3: 0x00,
// fyne.KeyF4: 0x00,
// fyne.KeyF5: 0x00,
// fyne.KeyF6: 0x00,
// fyne.KeyF7: 0x00,
// fyne.KeyF8: 0x00,
// fyne.KeyF9: 0x00,
// fyne.KeyF10: 0x00,
// fyne.KeyF11: 0x00,
// fyne.KeyF12: 0x00,
// fyne.KeyEnter: 0x0D,
// fyne.Key0: 0x30,
// fyne.Key1: 0x31,
// fyne.Key2: 0x32,
// fyne.Key3: 0x33,
// fyne.Key4: 0x34,
// fyne.Key5: 0x35,
// fyne.Key6: 0x36,
// fyne.Key7: 0x37,
// fyne.Key8: 0x38,
// fyne.Key9: 0x39,
// fyne.KeyA: 0x61,
// fyne.KeyB: 0x62,
// fyne.KeyC: 0x63,
// fyne.KeyD: 0x64,
// fyne.KeyE: 0x65,
// fyne.KeyF: 0x66,
// fyne.KeyG: 0x67,
// fyne.KeyH: 0x68,
// fyne.KeyI: 0x69,
// fyne.KeyJ: 0x6a,
// fyne.KeyK: 0x6b,
// fyne.KeyL: 0x6c,
// fyne.KeyM: 0x6d,
// fyne.KeyN: 0x6e,
// fyne.KeyO: 0x6f,
// fyne.KeyP: 0x70,
// fyne.KeyQ: 0x71,
// fyne.KeyR: 0x72,
// fyne.KeyS: 0x73,
// fyne.KeyT: 0x74,
// fyne.KeyU: 0x75,
// fyne.KeyV: 0x76,
// fyne.KeyW: 0x77,
// fyne.KeyX: 0x78,
// fyne.KeyY: 0x79,
// fyne.KeyZ: 0x7A,
// fyne.KeySpace: 0x20,
// fyne.KeyApostrophe: 0x27,
// fyne.KeyComma: 0x2c,
// fyne.KeyMinus: 0x2d,
// fyne.KeyPeriod: 0x2E,
// fyne.KeySlash: 0x2F,
// fyne.KeyBackslash: 0x5C,
// fyne.KeyLeftBracket: 0x5B,
// fyne.KeyRightBracket: 0x5D,
// fyne.KeySemicolon: 0x3B,
// fyne.KeyEqual: 0x3D,
// fyne.KeyAsterisk: 0x2A,
// fyne.KeyPlus: 0x2B,
// fyne.KeyBackTick: 0x60,
// fyne.KeyUnknown: 0x00,
//}
//
//var RemapKeyShift = map[fyne.KeyName]byte{
// fyne.KeyEscape: 0x1B,
// fyne.KeyReturn: 0x0A,
// fyne.KeyTab: 0x09,
// fyne.KeyBackspace: 0x08,
// fyne.KeyInsert: 0x00,
// fyne.KeyDelete: 0x08,
// fyne.KeyRight: 0x18,
// fyne.KeyLeft: 0x08,
// fyne.KeyDown: 0x0A,
// fyne.KeyUp: 0x19,
// fyne.KeyPageUp: 0x00,
// fyne.KeyPageDown: 0x00,
// fyne.KeyHome: 0x0C,
// fyne.KeyEnd: 0x1A,
// fyne.KeyF1: 0x00,
// fyne.KeyF2: 0x00,
// fyne.KeyF3: 0x00,
// fyne.KeyF4: 0x00,
// fyne.KeyF5: 0x00,
// fyne.KeyF6: 0x00,
// fyne.KeyF7: 0x00,
// fyne.KeyF8: 0x00,
// fyne.KeyF9: 0x00,
// fyne.KeyF10: 0x00,
// fyne.KeyF11: 0x00,
// fyne.KeyF12: 0x00,
// fyne.KeyEnter: 0x0D,
//
// fyne.Key0: 0x29,
// fyne.Key1: 0x21,
// fyne.Key2: 0x40,
// fyne.Key3: 0x23,
// fyne.Key4: 0x24,
// fyne.Key5: 0x25,
// fyne.Key6: 0x5E,
// fyne.Key7: 0x26,
// fyne.Key8: 0x2A,
// fyne.Key9: 0x28,
// fyne.KeyA: 0x41,
// fyne.KeyB: 0x42,
// fyne.KeyC: 0x43,
// fyne.KeyD: 0x44,
// fyne.KeyE: 0x45,
// fyne.KeyF: 0x46,
// fyne.KeyG: 0x47,
// fyne.KeyH: 0x48,
// fyne.KeyI: 0x49,
// fyne.KeyJ: 0x4a,
// fyne.KeyK: 0x4b,
// fyne.KeyL: 0x4c,
// fyne.KeyM: 0x4d,
// fyne.KeyN: 0x4e,
// fyne.KeyO: 0x4f,
// fyne.KeyP: 0x50,
// fyne.KeyQ: 0x51,
// fyne.KeyR: 0x52,
// fyne.KeyS: 0x53,
// fyne.KeyT: 0x54,
// fyne.KeyU: 0x55,
// fyne.KeyV: 0x56,
// fyne.KeyW: 0x57,
// fyne.KeyX: 0x58,
// fyne.KeyY: 0x59,
// fyne.KeyZ: 0x5A,
// fyne.KeySpace: 0x20,
// fyne.KeyApostrophe: 0x22,
// fyne.KeyComma: 0x3C,
// fyne.KeyMinus: 0x5F,
// fyne.KeyPeriod: 0x3E,
// fyne.KeySlash: 0x3F,
// fyne.KeyBackslash: 0x7C,
// fyne.KeyLeftBracket: 0x7B,
// fyne.KeyRightBracket: 0x7D,
// fyne.KeySemicolon: 0x3A,
// fyne.KeyEqual: 0x2B,
// fyne.KeyAsterisk: 0x7E,
// fyne.KeyPlus: 0x7E,
// fyne.KeyBackTick: 0x60,
//}

View File

@ -1,153 +0,0 @@
// Package usart
// Universal Serial Asynchronous Receiver/Transmitter
// i8051, MSM82C51, КР580ВВ51
// By Romych, 2025.03.04
package usart
import log "github.com/sirupsen/logrus"
//const I8251DSRFlag = 0x80
//const I8251SynDetFlag = 0x40
//const I8251FrameErrorFlag = 0x20
//const I8251OverrunErrorFlag = 0x10
//const I8251ParityErrorFlag = 0x08
const I8251TxEnableFlag = 0x04
const I8251RxReadyFlag = 0x02
const I8251TxReadyFlag = 0x01
const I8251TxBuffMaxLen = 16
const (
Sio8251Reset = iota
Sio8251LoadSyncChar1
Sio8251LoadSyncChar2
Sio8251LoadCommand
)
type I8251 struct {
counter uint64
mode byte
initState byte
syncChar1 byte
syncChar2 byte
bufferRx []byte
bufferTx []byte
rxe bool
txe bool
}
type I8251Interface interface {
Tick()
Status() byte
Reset()
Command(value byte)
Send(value byte)
Receive() byte
}
func New() *I8251 {
return &I8251{
counter: 0,
mode: 0,
initState: 0,
rxe: false,
txe: false,
bufferRx: []byte{},
bufferTx: []byte{},
}
}
func (s *I8251) Tick() {
s.counter++
}
// Status i8251 status [RST,RQ_RX,RST_ERR,PAUSE,RX_EN,RX_RDY,TX_RDY]
func (s *I8251) Status() byte {
var status byte = 0
if len(s.bufferRx) > 0 {
status |= I8251RxReadyFlag
}
if len(s.bufferTx) < I8251TxBuffMaxLen {
status |= I8251TxReadyFlag
}
if s.txe {
status |= I8251TxEnableFlag
}
return status
}
func (s *I8251) Reset() {
s.counter = 0
s.mode = 0
s.initState = 0
s.bufferRx = make([]byte, 8)
s.bufferTx = make([]byte, I8251TxBuffMaxLen)
s.rxe = false
s.txe = false
}
func (s *I8251) Command(value byte) {
switch s.initState {
case Sio8251Reset:
s.mode = value
if s.mode&0x03 > 0 {
// SYNC
s.initState = Sio8251LoadSyncChar1
}
// ASYNC
s.initState = Sio8251LoadCommand
case Sio8251LoadSyncChar1:
s.mode = value
if s.mode&0x80 == 0 { // SYNC DOUBLE
s.initState = Sio8251LoadSyncChar2
}
case Sio8251LoadSyncChar2:
s.mode = value
s.initState = Sio8251LoadCommand
case Sio8251LoadCommand:
// value = command
if value&0x40 != 0 {
// RESET CMD
s.Reset()
} else {
// Set RXE, TXE
if value&0x04 != 0 {
s.rxe = true
} else {
s.rxe = false
}
if value&0x01 != 0 {
s.txe = true
} else {
s.txe = false
}
}
}
}
func (s *I8251) Send(value byte) {
if s.txe {
s.bufferTx = append(s.bufferTx, value)
log.Debugf("Send byte: %x", value)
}
}
func (s *I8251) Receive() byte {
if s.rxe {
if len(s.bufferRx) > 0 {
res := s.bufferRx[0]
s.bufferRx = s.bufferRx[1:]
log.Debugf("Receive: %x", res)
return res
}
log.Debugf("Receive: empty buffer")
} else {
log.Debugf("Receive: receiver disabled")
}
return 0
}
func (s *I8251) SetRxBytes(bytes []byte) {
s.bufferRx = append(s.bufferRx, bytes...)
}

101
z80em/constants.go Normal file
View File

@ -0,0 +1,101 @@
package z80em
const OpHalt = 0x76
const OpLdBB = 0x40
const OpAddAB = 0x80
const OpRetNz = 0xc0
var CycleCounts = []byte{
4, 10, 7, 6, 4, 4, 7, 4, 4, 11, 7, 6, 4, 4, 7, 4,
8, 10, 7, 6, 4, 4, 7, 4, 12, 11, 7, 6, 4, 4, 7, 4,
7, 10, 16, 6, 4, 4, 7, 4, 7, 11, 16, 6, 4, 4, 7, 4,
7, 10, 13, 6, 11, 11, 10, 4, 7, 11, 13, 6, 4, 4, 7, 4,
4, 4, 4, 4, 4, 4, 7, 4, 4, 4, 4, 4, 4, 4, 7, 4,
4, 4, 4, 4, 4, 4, 7, 4, 4, 4, 4, 4, 4, 4, 7, 4,
4, 4, 4, 4, 4, 4, 7, 4, 4, 4, 4, 4, 4, 4, 7, 4,
7, 7, 7, 7, 7, 7, 4, 7, 4, 4, 4, 4, 4, 4, 7, 4,
4, 4, 4, 4, 4, 4, 7, 4, 4, 4, 4, 4, 4, 4, 7, 4,
4, 4, 4, 4, 4, 4, 7, 4, 4, 4, 4, 4, 4, 4, 7, 4,
4, 4, 4, 4, 4, 4, 7, 4, 4, 4, 4, 4, 4, 4, 7, 4,
4, 4, 4, 4, 4, 4, 7, 4, 4, 4, 4, 4, 4, 4, 7, 4,
5, 10, 10, 10, 10, 11, 7, 11, 5, 10, 10, 0, 10, 17, 7, 11,
5, 10, 10, 11, 10, 11, 7, 11, 5, 4, 10, 11, 10, 0, 7, 11,
5, 10, 10, 19, 10, 11, 7, 11, 5, 4, 10, 4, 10, 0, 7, 11,
5, 10, 10, 4, 10, 11, 7, 11, 5, 6, 10, 4, 10, 0, 7, 11,
}
var CycleCountsCb = []byte{
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8,
8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8,
8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8,
8, 8, 8, 8, 8, 8, 12, 8, 8, 8, 8, 8, 8, 8, 12, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
8, 8, 8, 8, 8, 8, 15, 8, 8, 8, 8, 8, 8, 8, 15, 8,
}
var CycleCountsDd = []byte{
0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 15, 0, 0, 0, 0, 0, 0,
0, 14, 20, 10, 8, 8, 11, 0, 0, 15, 20, 10, 8, 8, 11, 0,
0, 0, 0, 0, 23, 23, 19, 0, 0, 15, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 8, 8, 19, 0, 0, 0, 0, 0, 8, 8, 19, 0,
0, 0, 0, 0, 8, 8, 19, 0, 0, 0, 0, 0, 8, 8, 19, 0,
8, 8, 8, 8, 8, 8, 19, 8, 8, 8, 8, 8, 8, 8, 19, 8,
19, 19, 19, 19, 19, 19, 0, 19, 0, 0, 0, 0, 8, 8, 19, 0,
0, 0, 0, 0, 8, 8, 19, 0, 0, 0, 0, 0, 8, 8, 19, 0,
0, 0, 0, 0, 8, 8, 19, 0, 0, 0, 0, 0, 8, 8, 19, 0,
0, 0, 0, 0, 8, 8, 19, 0, 0, 0, 0, 0, 8, 8, 19, 0,
0, 0, 0, 0, 8, 8, 19, 0, 0, 0, 0, 0, 8, 8, 19, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 14, 0, 23, 0, 15, 0, 0, 0, 8, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 0, 0, 0, 0,
}
var CycleCountsEd = []byte{
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
12, 12, 15, 20, 8, 14, 8, 9, 12, 12, 15, 20, 8, 14, 8, 9,
12, 12, 15, 20, 8, 14, 8, 9, 12, 12, 15, 20, 8, 14, 8, 9,
12, 12, 15, 20, 8, 14, 8, 18, 12, 12, 15, 20, 8, 14, 8, 18,
12, 12, 15, 20, 8, 14, 8, 0, 12, 12, 15, 20, 8, 14, 8, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
16, 16, 16, 16, 0, 0, 0, 0, 16, 16, 16, 16, 0, 0, 0, 0,
16, 16, 16, 16, 0, 0, 0, 0, 16, 16, 16, 16, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
}
var ParityBits = []bool{
true, false, false, true, false, true, true, false, false, true, true, false, true, false, false, true,
false, true, true, false, true, false, false, true, true, false, false, true, false, true, true, false,
false, true, true, false, true, false, false, true, true, false, false, true, false, true, true, false,
true, false, false, true, false, true, true, false, false, true, true, false, true, false, false, true,
false, true, true, false, true, false, false, true, true, false, false, true, false, true, true, false,
true, false, false, true, false, true, true, false, false, true, true, false, true, false, false, true,
true, false, false, true, false, true, true, false, false, true, true, false, true, false, false, true,
false, true, true, false, true, false, false, true, true, false, false, true, false, true, true, false,
false, true, true, false, true, false, false, true, true, false, false, true, false, true, true, false,
true, false, false, true, false, true, true, false, false, true, true, false, true, false, false, true,
true, false, false, true, false, true, true, false, false, true, true, false, true, false, false, true,
false, true, true, false, true, false, false, true, true, false, false, true, false, true, true, false,
true, false, false, true, false, true, true, false, false, true, true, false, true, false, false, true,
false, true, true, false, true, false, false, true, true, false, false, true, false, true, true, false,
false, true, true, false, true, false, false, true, true, false, false, true, false, true, true, false,
true, false, false, true, false, true, true, false, false, true, true, false, true, false, false, true,
}

657
z80em/opcode.go Normal file
View File

@ -0,0 +1,657 @@
package z80em
var instructions = []func(s *Z80Type){
// NOP
0x00: func(s *Z80Type) {
// NOP
},
// LD BC, nn
0x01: func(s *Z80Type) {
s.PC++
s.C = s.core.MemRead(s.PC)
s.PC++
s.B = s.core.MemRead(s.PC)
},
// LD (BC), A
0x02: func(s *Z80Type) {
s.core.MemWrite(s.bc(), s.A)
},
// 0x03 : INC BC
0x03: func(s *Z80Type) {
s.incBc()
},
// 0x04 : INC B
0x04: func(s *Z80Type) {
s.B = s.doInc(s.B)
},
// 0x05 : DEC B
0x05: func(s *Z80Type) {
s.B = s.doDec(s.B)
},
// 0x06 : LD B, n
0x06: func(s *Z80Type) {
s.PC++
s.B = s.core.MemRead(s.PC)
},
// 0x07 : RLCA
0x07: func(s *Z80Type) {
// This instruction is implemented as a special case of the
// more general Z80-specific RLC instruction.
// Specifially, RLCA is a version of RLC A that affects fewer flags.
// The same applies to RRCA, RLA, and RRA.
tempS := s.Flags.S
tempZ := s.Flags.Z
tempP := s.Flags.P
s.A = s.doRlc(s.A)
s.Flags.S = tempS
s.Flags.Z = tempZ
s.Flags.P = tempP
},
// 0x08 : EX AF, AF'
0x08: func(s *Z80Type) {
s.A, s.APrime = s.APrime, s.A
temp := s.getFlagsRegister()
s.setFlagsRegister(s.getFlagsPrimeRegister())
s.setFlagsPrimeRegister(temp)
},
// 0x09 : ADD HL, BC
0x09: func(s *Z80Type) {
s.doHlAdd(s.bc())
},
// 0x0a : LD A, (BC)
0x0A: func(s *Z80Type) {
s.A = s.core.MemRead(s.bc())
},
// 0x0b : DEC BC
0x0B: func(s *Z80Type) {
s.decBc()
},
// 0x0c : INC C
0x0C: func(s *Z80Type) {
s.C = s.doInc(s.C)
},
// 0x0d : DEC C
0x0D: func(s *Z80Type) {
s.C = s.doDec(s.C)
},
// 0x0e : LD C, n
0x0E: func(s *Z80Type) {
s.PC++
s.C = s.core.MemRead(s.PC)
},
// 0x0f : RRCA
0x0F: func(s *Z80Type) {
tempS := s.Flags.S
tempZ := s.Flags.Z
tempP := s.Flags.P
s.A = s.doRrc(s.A)
s.Flags.S = tempS
s.Flags.Z = tempZ
s.Flags.P = tempP
},
// 0x10 : DJNZ nn
0x10: func(s *Z80Type) {
s.B--
s.doConditionalRelativeJump(s.B != 0)
},
// 0x11 : LD DE, nn
0x11: func(s *Z80Type) {
s.PC++
s.E = s.core.MemRead(s.PC)
s.PC++
s.D = s.core.MemRead(s.PC)
},
// 0x12 : LD (DE), A
0x12: func(s *Z80Type) {
s.core.MemWrite(s.de(), s.A)
},
// 0x13 : INC DE
0x13: func(s *Z80Type) {
s.incDe()
},
// 0x14 : INC D
0x14: func(s *Z80Type) {
s.D = s.doInc(s.D)
},
// 0x15 : DEC D
0x15: func(s *Z80Type) {
s.D = s.doDec(s.D)
},
// 0x16 : LD D, n
0x16: func(s *Z80Type) {
s.PC++
s.D = s.core.MemRead(s.PC)
},
// 0x17 : RLA
0x17: func(s *Z80Type) {
tempS := s.Flags.S
tempZ := s.Flags.Z
tempP := s.Flags.P
s.A = s.doRl(s.A)
s.Flags.S = tempS
s.Flags.Z = tempZ
s.Flags.P = tempP
},
// 0x18 : JR n
0x18: func(s *Z80Type) {
var o = int8(s.core.MemRead(s.PC + 1))
if o > 0 {
s.PC += uint16(o)
} else {
s.PC -= uint16(-o)
}
s.PC++
},
// 0x19 : ADD HL, DE
0x19: func(s *Z80Type) {
s.doHlAdd(s.de())
},
// 0x1a : LD A, (DE)
0x1A: func(s *Z80Type) {
s.A = s.core.MemRead(s.de())
},
// 0x1b : DEC DE
0x1B: func(s *Z80Type) {
s.decDe()
},
// 0x1c : INC E
0x1C: func(s *Z80Type) {
s.E = s.doInc(s.E)
},
// 0x1d : DEC E
0x1D: func(s *Z80Type) {
s.E = s.doDec(s.E)
},
// 0x1e : LD E, n
0x1E: func(s *Z80Type) {
s.PC++
s.E = s.core.MemRead(s.PC)
},
// 0x1f : RRA
0x1F: func(s *Z80Type) {
tempS := s.Flags.S
tempZ := s.Flags.Z
tempP := s.Flags.P
s.A = s.doRr(s.A)
s.Flags.S = tempS
s.Flags.Z = tempZ
s.Flags.P = tempP
},
// 0x20 : JR NZ, n
0x20: func(s *Z80Type) {
s.doConditionalRelativeJump(!s.Flags.Z)
},
// 0x21 : LD HL, nn
0x21: func(s *Z80Type) {
s.PC++
s.L = s.core.MemRead(s.PC)
s.PC++
s.H = s.core.MemRead(s.PC)
},
// 0x22 : LD (nn), HL
0x22: func(s *Z80Type) {
addr := s.getAddr()
s.core.MemWrite(addr, s.L)
s.core.MemWrite(addr+1, s.H)
},
// 0x23 : INC HL
0x23: func(s *Z80Type) {
s.incHl()
},
// 0x24 : INC H
0x24: func(s *Z80Type) {
s.H = s.doInc(s.H)
},
// 0x25 : DEC H
0x25: func(s *Z80Type) {
s.H = s.doDec(s.H)
},
// 0x26 : LD H, n
0x26: func(s *Z80Type) {
s.PC++
s.H = s.core.MemRead(s.PC)
},
// 0x27 : DAA
0x27: func(s *Z80Type) {
temp := s.A
if !s.Flags.N {
if s.Flags.H || ((s.A & 0x0f) > 9) {
temp += 0x06
}
if s.Flags.C || (s.A > 0x99) {
temp += 0x60
}
} else {
if s.Flags.H || ((s.A & 0x0f) > 9) {
temp -= 0x06
}
if s.Flags.C || (s.A > 0x99) {
temp -= 0x60
}
}
s.Flags.S = (temp & 0x80) != 0
s.Flags.Z = temp == 0
s.Flags.H = ((s.A & 0x10) ^ (temp & 0x10)) != 0
s.Flags.P = ParityBits[temp]
// DAA never clears the carry flag if it was already set,
// but it is able to set the carry flag if it was clear.
// Don't ask me, I don't know.
// Note also that we check for a BCD carry, instead of the usual.
s.Flags.C = s.Flags.C || (s.A > 0x99)
s.A = temp
s.updateXYFlags(s.A)
},
// 0x28 : JR Z, n
0x28: func(s *Z80Type) {
s.doConditionalRelativeJump(s.Flags.Z)
},
// 0x29 : ADD HL, HL
0x29: func(s *Z80Type) {
s.doHlAdd(s.hl())
},
// 0x2a : LD HL, (nn)
0x2A: func(s *Z80Type) {
addr := s.getAddr()
s.L = s.core.MemRead(addr)
s.H = s.core.MemRead(addr + 1)
},
// 0x2b : DEC HL
0x2B: func(s *Z80Type) {
s.decHl()
},
// 0x2c : INC L
0x2C: func(s *Z80Type) {
s.L = s.doInc(s.L)
},
// 0x2d : DEC L
0x2D: func(s *Z80Type) {
s.L = s.doDec(s.L)
},
// 0x2e : LD L, n
0x2E: func(s *Z80Type) {
s.PC++
s.L = s.core.MemRead(s.PC)
},
// 0x2f : CPL
0x2F: func(s *Z80Type) {
s.A = ^s.A
s.Flags.N = true
s.Flags.H = true
s.updateXYFlags(s.A)
},
// 0x30 : JR NC, n
0x30: func(s *Z80Type) {
s.doConditionalRelativeJump(!s.Flags.C)
},
// 0x31 : LD SP, nn
0x31: func(s *Z80Type) {
s.PC++
lo := s.core.MemRead(s.PC)
s.PC++
s.SP = (uint16(s.core.MemRead(s.PC)) << 8) | uint16(lo)
},
// 0x32 : LD (nn), A
0x32: func(s *Z80Type) {
s.core.MemWrite(s.getAddr(), s.A)
},
// 0x33 : INC SP
0x33: func(s *Z80Type) {
s.SP++
},
// 0x34 : INC (HL)
0x34: func(s *Z80Type) {
s.core.MemWrite(s.hl(), s.doInc(s.core.MemRead(s.hl())))
},
// 0x35 : DEC (HL)
0x35: func(s *Z80Type) {
s.core.MemWrite(s.hl(), s.doDec(s.core.MemRead(s.hl())))
},
// 0x36 : LD (HL), n
0x36: func(s *Z80Type) {
s.PC++
s.core.MemWrite(s.hl(), s.core.MemRead(s.PC))
},
// 0x37 : SCF
0x37: func(s *Z80Type) {
s.Flags.N = false
s.Flags.H = false
s.Flags.C = true
s.updateXYFlags(s.A)
},
// 0x38 : JR C, n
0x38: func(s *Z80Type) {
s.doConditionalRelativeJump(s.Flags.C)
},
// 0x39 : ADD HL, SP
0x39: func(s *Z80Type) {
s.doHlAdd(s.SP)
},
// 0x3a : LD A, (nn)
0x3A: func(s *Z80Type) {
s.A = s.core.MemRead(s.getAddr())
},
// 0x3b : DEC SP
0x3B: func(s *Z80Type) {
s.SP--
},
// 0x3c : INC A
0x3C: func(s *Z80Type) {
s.A = s.doInc(s.A)
},
// 0x3d : DEC A
0x3D: func(s *Z80Type) {
s.A = s.doDec(s.A)
},
// 0x3e : LD A, n
0x3E: func(s *Z80Type) {
s.PC++
s.A = s.core.MemRead(s.PC)
},
// 0x3f : CCF
0x3F: func(s *Z80Type) {
s.Flags.N = false
s.Flags.H = s.Flags.C
s.Flags.C = !s.Flags.C
s.updateXYFlags(s.A)
},
// 0xc0 : RET NZ
0xC0: func(s *Z80Type) {
s.doConditionalReturn(!s.Flags.Z)
},
// 0xc1 : POP BC
0xC1: func(s *Z80Type) {
result := s.PopWord()
s.C = byte(result & 0xff)
s.B = byte((result & 0xff00) >> 8)
},
// 0xc2 : JP NZ, nn
0xC2: func(s *Z80Type) {
s.doConditionalAbsoluteJump(!s.Flags.Z)
},
// 0xc3 : JP nn
0xC3: func(s *Z80Type) {
s.PC = uint16(s.core.MemRead(s.PC+1)) | (uint16(s.core.MemRead(s.PC+2)) << 8)
s.PC--
},
// 0xc4 : CALL NZ, nn
0xC4: func(s *Z80Type) {
s.doConditionalCall(!s.Flags.Z)
},
// 0xc5 : PUSH BC
0xC5: func(s *Z80Type) {
s.pushWord((uint16(s.B) << 8) | uint16(s.C))
},
// 0xc6 : ADD A, n
0xC6: func(s *Z80Type) {
s.PC++
s.doAdd(s.core.MemRead(s.PC))
},
// 0xc7 : RST 00h
0xC7: func(s *Z80Type) {
s.doReset(0x0000)
},
// 0xc8 : RET Z
0xC8: func(s *Z80Type) {
s.doConditionalReturn(s.Flags.Z)
},
// 0xc9 : RET
0xC9: func(s *Z80Type) {
s.PC = s.PopWord() - 1
},
// 0xca : JP Z, nn
0xCA: func(s *Z80Type) {
s.doConditionalAbsoluteJump(s.Flags.Z)
},
// 0xcb : CB Prefix
0xCB: func(s *Z80Type) {
s.opcodeCB()
},
// 0xcc : CALL Z, nn
0xCC: func(s *Z80Type) {
s.doConditionalCall(s.Flags.Z)
},
// 0xcd : CALL nn
0xCD: func(s *Z80Type) {
s.pushWord(s.PC + 3)
s.PC = uint16(s.core.MemRead(s.PC+1)) | (uint16(s.core.MemRead(s.PC+2)) << 8)
s.PC--
},
// 0xce : ADC A, n
0xCE: func(s *Z80Type) {
s.PC++
s.doAdc(s.core.MemRead(s.PC))
},
// 0xcf : RST 08h
0xCF: func(s *Z80Type) {
s.doReset(0x0008)
},
// 0xd0 : RET NC
0xD0: func(s *Z80Type) {
s.doConditionalReturn(!s.Flags.C)
},
// 0xd1 : POP DE
0xD1: func(s *Z80Type) {
result := s.PopWord()
s.E = byte(result & 0xff)
s.D = byte((result & 0xff00) >> 8)
},
// 0xd2 : JP NC, nn
0xD2: func(s *Z80Type) {
s.doConditionalAbsoluteJump(!s.Flags.C)
},
// 0xd3 : OUT (n), A
0xD3: func(s *Z80Type) {
s.PC++
s.core.IOWrite((uint16(s.A)<<8)|uint16(s.core.MemRead(s.PC)), s.A)
},
// 0xd4 : CALL NC, nn
0xD4: func(s *Z80Type) {
s.doConditionalCall(!s.Flags.C)
},
// 0xd5 : PUSH DE
0xD5: func(s *Z80Type) {
s.pushWord((uint16(s.D) << 8) | uint16(s.E))
},
// 0xd6 : SUB n
0xD6: func(s *Z80Type) {
s.PC++
s.doSub(s.core.MemRead(s.PC))
},
// 0xd7 : RST 10h
0xD7: func(s *Z80Type) {
s.doReset(0x0010)
},
// 0xd8 : RET C
0xD8: func(s *Z80Type) {
s.doConditionalReturn(s.Flags.C)
},
// 0xd9 : EXX
0xD9: func(s *Z80Type) {
s.B, s.BPrime = s.BPrime, s.B
s.C, s.CPrime = s.CPrime, s.C
s.D, s.DPrime = s.DPrime, s.D
s.E, s.EPrime = s.EPrime, s.E
s.H, s.HPrime = s.HPrime, s.H
s.L, s.LPrime = s.LPrime, s.L
},
// 0xda : JP C, nn
0xDA: func(s *Z80Type) {
s.doConditionalAbsoluteJump(s.Flags.C)
},
// 0xdb : IN A, (n)
0xDB: func(s *Z80Type) {
s.PC++
s.A = s.core.IORead((uint16(s.A) << 8) | uint16(s.core.MemRead(s.PC)))
},
// 0xdc : CALL C, nn
0xDC: func(s *Z80Type) {
s.doConditionalCall(s.Flags.C)
},
// 0xdd : DD Prefix (IX instructions)
0xDD: func(s *Z80Type) {
s.opcodeDD()
},
// 0xde : SBC n
0xDE: func(s *Z80Type) {
s.PC++
s.doSbc(s.core.MemRead(s.PC))
},
// 0xdf : RST 18h
0xDF: func(s *Z80Type) {
s.doReset(0x0018)
},
// 0xe0 : RET PO
0xE0: func(s *Z80Type) {
s.doConditionalReturn(!s.Flags.P)
},
// 0xe1 : POP HL
0xE1: func(s *Z80Type) {
result := s.PopWord()
s.L = byte(result & 0xff)
s.H = byte((result & 0xff00) >> 8)
},
// 0xe2 : JP PO, (nn)
0xE2: func(s *Z80Type) {
s.doConditionalAbsoluteJump(!s.Flags.P)
},
// 0xe3 : EX (SP), HL
0xE3: func(s *Z80Type) {
temp := s.core.MemRead(s.SP)
s.core.MemWrite(s.SP, s.L)
s.L = temp
temp = s.core.MemRead(s.SP + 1)
s.core.MemWrite(s.SP+1, s.H)
s.H = temp
},
// 0xe4 : CALL PO, nn
0xE4: func(s *Z80Type) {
s.doConditionalCall(!s.Flags.P)
},
// 0xe5 : PUSH HL
0xE5: func(s *Z80Type) {
s.pushWord((uint16(s.H) << 8) | uint16(s.L))
},
// 0xe6 : AND n
0xE6: func(s *Z80Type) {
s.PC++
s.doAnd(s.core.MemRead(s.PC))
},
// 0xe7 : RST 20h
0xE7: func(s *Z80Type) {
s.doReset(0x0020)
},
// 0xe8 : RET PE
0xE8: func(s *Z80Type) {
s.doConditionalReturn(s.Flags.P)
},
// 0xe9 : JP (HL)
0xE9: func(s *Z80Type) {
s.PC = uint16(s.H)<<8 | uint16(s.L)
s.PC--
},
// 0xea : JP PE, nn
0xEA: func(s *Z80Type) {
s.doConditionalAbsoluteJump(s.Flags.P)
},
// 0xeb : EX DE, HL
0xEB: func(s *Z80Type) {
s.D, s.H = s.H, s.D
s.E, s.L = s.L, s.E
},
// 0xec : CALL PE, nn
0xEC: func(s *Z80Type) {
s.doConditionalCall(s.Flags.P)
},
// 0xed : ED Prefix
0xED: func(s *Z80Type) {
s.opcodeED()
},
// 0xee : XOR n
0xEE: func(s *Z80Type) {
s.PC++
s.doXor(s.core.MemRead(s.PC))
},
// 0xef : RST 28h
0xEF: func(s *Z80Type) {
s.doReset(0x0028)
},
// 0xf0 : RET P
0xF0: func(s *Z80Type) {
s.doConditionalReturn(!s.Flags.S)
},
// 0xf1 : POP AF
0xF1: func(s *Z80Type) {
var result = s.PopWord()
s.setFlagsRegister(byte(result & 0xff))
s.A = byte((result & 0xff00) >> 8)
},
// 0xf2 : JP P, nn
0xF2: func(s *Z80Type) {
s.doConditionalAbsoluteJump(!s.Flags.S)
},
// 0xf3 : DI
0xF3: func(s *Z80Type) {
// DI doesn't actually take effect until after the next instruction.
s.DoDelayedDI = true
},
// 0xf4 : CALL P, nn
0xF4: func(s *Z80Type) {
s.doConditionalCall(!s.Flags.S)
},
// 0xf5 : PUSH AF
0xF5: func(s *Z80Type) {
s.pushWord(uint16(s.getFlagsRegister()) | (uint16(s.A) << 8))
},
// 0xf6 : OR n
0xF6: func(s *Z80Type) {
s.PC++
s.doOr(s.core.MemRead(s.PC))
},
// 0xf7 : RST 30h
0xF7: func(s *Z80Type) {
s.doReset(0x0030)
},
// 0xf8 : RET M
0xF8: func(s *Z80Type) {
s.doConditionalReturn(s.Flags.S)
},
// 0xf9 : LD SP, HL
0xF9: func(s *Z80Type) {
s.SP = uint16(s.H)<<8 | uint16(s.L)
},
// 0xfa : JP M, nn
0xFA: func(s *Z80Type) {
s.doConditionalAbsoluteJump(s.Flags.S)
},
// 0xfb : EI
0xFB: func(s *Z80Type) {
// EI doesn't actually take effect until after the next instruction.
s.DoDelayedEI = true
},
// 0xfc : CALL M, nn
0xFC: func(s *Z80Type) {
s.doConditionalCall(s.Flags.S)
},
// 0xfd : FD Prefix (IY instructions)
0xFD: func(s *Z80Type) {
s.opcodeFD()
},
// 0xfe : CP n
0xFE: func(s *Z80Type) {
s.PC++
s.doCp(s.core.MemRead(s.PC))
},
// 0xff : RST 38h
0xFF: func(s *Z80Type) {
s.doReset(0x0038)
},
}
func (z *Z80Type) getAddr() uint16 {
z.PC++
addr := uint16(z.core.MemRead(z.PC))
z.PC++
addr |= uint16(z.core.MemRead(z.PC)) << 8
return addr
}

106
z80em/opcodeCB.go Normal file
View File

@ -0,0 +1,106 @@
package z80em
func (z *Z80Type) opcodeCB() {
z.R = (z.R & 0x80) | (((z.R & 0x7f) + 1) & 0x7f)
z.PC++
opcode := z.core.M1MemRead(z.PC)
bitNumber := (opcode & 0x38) >> 3
regCode := opcode & 0x07
if opcode < 0x40 {
// Shift/rotate instructions
opArray := []OpShift{z.doRlc, z.doRrc, z.doRl, z.doRr, z.doSla, z.doSra, z.doSll, z.doSrl}
switch regCode {
case 0:
z.B = opArray[bitNumber](z.B)
case 1:
z.C = opArray[bitNumber](z.C)
case 2:
z.D = opArray[bitNumber](z.D)
case 3:
z.E = opArray[bitNumber](z.E)
case 4:
z.H = opArray[bitNumber](z.H)
case 5:
z.L = opArray[bitNumber](z.L)
case 6:
z.core.MemWrite(z.hl(), opArray[bitNumber](z.core.MemRead(z.hl())))
default:
z.A = opArray[bitNumber](z.A)
}
} else if opcode < 0x80 {
// BIT instructions
switch regCode {
case 0:
z.Flags.Z = z.B&(1<<bitNumber) == 0
case 1:
z.Flags.Z = z.C&(1<<bitNumber) == 0
case 2:
z.Flags.Z = z.D&(1<<bitNumber) == 0
case 3:
z.Flags.Z = z.E&(1<<bitNumber) == 0
case 4:
z.Flags.Z = z.H&(1<<bitNumber) == 0
case 5:
z.Flags.Z = z.L&(1<<bitNumber) == 0
case 6:
z.Flags.Z = z.core.MemRead(z.hl())&(1<<bitNumber) == 0
default:
z.Flags.Z = z.A&(1<<bitNumber) == 0
}
z.Flags.N = false
z.Flags.H = true
z.Flags.P = z.Flags.Z
z.Flags.S = (bitNumber == 7) && !z.Flags.Z
// For the BIT n, (HL) instruction, the X and Y flags are obtained
// from what is apparently an internal temporary register used for
// some of the 16-bit arithmetic instructions.
// I haven't implemented that register here,
// so for now we'll set X and Y the same way for every BIT opcode,
// which means that they will usually be wrong for BIT n, (HL).
z.Flags.Y = (bitNumber == 5) && !z.Flags.Z
z.Flags.X = (bitNumber == 3) && !z.Flags.Z
} else if opcode < 0xC0 {
// RES instructions
negMask := byte(^(1 << bitNumber))
switch regCode {
case 0:
z.B &= negMask
case 1:
z.C &= negMask
case 2:
z.D &= negMask
case 3:
z.E &= negMask
case 4:
z.H &= negMask
case 5:
z.L &= negMask
case 6:
z.core.MemWrite(z.hl(), z.core.MemRead(z.hl())&negMask)
default:
z.A &= negMask
}
} else {
// SET instructions
mask := byte(1 << bitNumber)
switch regCode {
case 0:
z.B |= mask
case 1:
z.C |= mask
case 2:
z.D |= mask
case 3:
z.E |= mask
case 4:
z.H |= mask
case 5:
z.L |= mask
case 6:
z.core.MemWrite(z.hl(), z.core.MemRead(z.hl())|mask)
default:
z.A |= mask
}
}
z.CycleCounter += CycleCountsCb[opcode]
}

483
z80em/opcodeDD.go Normal file
View File

@ -0,0 +1,483 @@
package z80em
var ddInstructions = []func(s *Z80Type){
// 0x09 : ADD IX, BC
0x09: func(s *Z80Type) {
s.doIxAdd(s.bc())
},
// 0x19 : ADD IX, DE
0x19: func(s *Z80Type) {
s.doIxAdd(s.de())
},
// 0x21 : LD IX, nn
0x21: func(s *Z80Type) {
s.IX = s.getAddr()
},
// 0x22 : LD (nn), IX
0x22: func(s *Z80Type) {
addr := s.getAddr()
s.core.MemWrite(addr, byte(s.IX&0x00ff))
s.core.MemWrite(addr+1, byte(s.IX>>8))
},
// 0x23 : INC IX
0x23: func(s *Z80Type) {
s.IX++
},
// 0x24 : INC IXH (Undocumented)
0x24: func(s *Z80Type) {
s.IX = (uint16(s.doInc(byte(s.IX>>8))) << 8) | (s.IX & 0x00ff)
},
// 0x25 : DEC IXH (Undocumented)
0x25: func(s *Z80Type) {
s.IX = (uint16(s.doDec(byte(s.IX>>8))) << 8) | (s.IX & 0x00ff)
},
// 0x26 : LD IXH, n (Undocumented)
0x26: func(s *Z80Type) {
s.PC++
s.IX = (uint16(s.core.MemRead(s.PC)) << 8) | (s.IX & 0x00ff)
},
// 0x29 : ADD IX, IX
0x29: func(s *Z80Type) {
s.doIxAdd(s.IX)
},
// 0x2a : LD IX, (nn)
0x2A: func(s *Z80Type) {
addr := s.getAddr()
s.IX = (uint16(s.core.MemRead(addr)) << 8) | uint16(s.core.MemRead(addr+1))
},
// 0x2b : DEC IX
0x2B: func(s *Z80Type) {
s.IX--
},
// 0x2c : INC IXL (Undocumented)
0x2C: func(s *Z80Type) {
s.IX = (uint16(s.doInc(byte(s.IX & 0x00ff)))) | (s.IX & 0xff00)
},
// 0x2d : DEC IXL (Undocumented)
0x2D: func(s *Z80Type) {
s.IX = (uint16(s.doDec(byte(s.IX & 0x00ff)))) | (s.IX & 0xff00)
},
// 0x2e : LD IXL, n (Undocumented)
0x2E: func(s *Z80Type) {
s.PC++
s.IX = (uint16(s.core.MemRead(s.PC))) | (s.IX & 0xff00)
},
// 0x34 : INC (IX+n)
0x34: func(s *Z80Type) {
offset := s.getOffset(s.IX)
value := s.core.MemRead(offset)
s.core.MemWrite(offset, s.doInc(value))
},
// 0x35 : DEC (IX+n)
0x35: func(s *Z80Type) {
offset := s.getOffset(s.IX)
value := s.core.MemRead(offset)
s.core.MemWrite(offset, s.doDec(value))
},
// 0x36 : LD (IX+n), n
0x36: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.PC++
s.core.MemWrite(offset, s.core.MemRead(s.PC))
},
// 0x39 : ADD IX, SP
0x39: func(s *Z80Type) {
s.doIxAdd(s.SP)
},
// 0x44 : LD B, IXH (Undocumented)
0x44: func(s *Z80Type) {
s.B = byte(s.IX >> 8)
},
// 0x45 : LD B, IXL (Undocumented)
0x45: func(s *Z80Type) {
s.B = byte(s.IX & 0x00ff)
},
// 0x46 : LD B, (IX+n)
0x46: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.B = s.core.MemRead(offset)
},
// 0x4c : LD C, IXH (Undocumented)
0x4C: func(s *Z80Type) {
s.C = byte(s.IX >> 8)
},
// 0x4d : LD C, IXL (Undocumented)
0x4D: func(s *Z80Type) {
s.C = byte(s.IX & 0x00ff)
},
// 0x4e : LD C, (IX+n)
0x4E: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.C = s.core.MemRead(offset)
},
// 0x54 : LD D, IXH (Undocumented)
0x54: func(s *Z80Type) {
s.D = byte(s.IX >> 8)
},
// 0x55 : LD D, IXL (Undocumented)
0x55: func(s *Z80Type) {
s.D = byte(s.IX & 0x00ff)
},
// 0x56 : LD D, (IX+n)
0x56: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.D = s.core.MemRead(offset)
},
// 0x5d : LD E, IXL (Undocumented)
0x5D: func(s *Z80Type) {
s.E = byte(s.IX & 0x00ff)
},
// 0x5e : LD E, (IX+n)
0x5E: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.E = s.core.MemRead(offset)
},
// 0x60 : LD IXH, B (Undocumented)
0x60: func(s *Z80Type) {
s.IX = uint16(s.B)<<8 | s.IX&0x00ff
},
// 0x61 : LD IXH, C (Undocumented)
0x61: func(s *Z80Type) {
s.IX = uint16(s.C)<<8 | s.IX&0x00ff
},
// 0x62 : LD IXH, D (Undocumented)
0x62: func(s *Z80Type) {
s.IX = uint16(s.D)<<8 | s.IX&0x00ff
},
// 0x63 : LD IXH, E (Undocumented)
0x63: func(s *Z80Type) {
s.IX = uint16(s.E)<<8 | s.IX&0x00ff
},
// 0x64 : LD IXH, IXH (Undocumented)
0x64: func(s *Z80Type) {
// NOP
},
// 0x65 : LD IXH, IXL (Undocumented)
0x65: func(s *Z80Type) {
s.IX = (s.IX << 8) | (s.IX & 0x00ff)
},
// 0x66 : LD H, (IX+n)
0x66: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.H = s.core.MemRead(offset)
},
// 0x67 : LD IXH, A (Undocumented)
0x67: func(s *Z80Type) {
s.IX = (uint16(s.A) << 8) | (s.IX & 0x00ff)
},
// 0x68 : LD IXL, B (Undocumented)
0x68: func(s *Z80Type) {
s.IX = (s.IX & 0xff00) | uint16(s.B)
},
// 0x69 : LD IXL, C (Undocumented)
0x69: func(s *Z80Type) {
s.IX = (s.IX & 0xff00) | uint16(s.C)
},
// 0x6a : LD IXL, D (Undocumented)
0x6a: func(s *Z80Type) {
s.IX = (s.IX & 0xff00) | uint16(s.D)
},
// 0x6b : LD IXL, E (Undocumented)
0x6b: func(s *Z80Type) {
s.IX = (s.IX & 0xff00) | uint16(s.E)
},
// 0x6c : LD IXL, IXH (Undocumented)
0x6c: func(s *Z80Type) {
s.IX = (s.IX >> 8) | (s.IX & 0xff00)
},
// 0x6d : LD IXL, IXL (Undocumented)
0x6d: func(s *Z80Type) {
// NOP
},
// 0x6e : LD L, (IX+n)
0x6e: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.L = s.core.MemRead(offset)
},
// 0x6f : LD IXL, A (Undocumented)
0x6f: func(s *Z80Type) {
s.IX = uint16(s.A) | (s.IX & 0xff00)
},
// 0x70 : LD (IX+n), B
0x70: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.core.MemWrite(offset, s.B)
},
// 0x71 : LD (IX+n), C
0x71: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.core.MemWrite(offset, s.C)
},
// 0x72 : LD (IX+n), D
0x72: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.core.MemWrite(offset, s.D)
},
// 0x73 : LD (IX+n), E
0x73: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.core.MemWrite(offset, s.E)
},
// 0x74 : LD (IX+n), H
0x74: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.core.MemWrite(offset, s.H)
},
// 0x75 : LD (IX+n), L
0x75: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.core.MemWrite(offset, s.L)
},
// 0x77 : LD (IX+n), A
0x77: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.core.MemWrite(offset, s.A)
},
// 0x7c : LD A, IXH (Undocumented)
0x7C: func(s *Z80Type) {
s.A = byte(s.IX >> 8)
},
// 0x7d : LD A, IXL (Undocumented)
0x7D: func(s *Z80Type) {
s.A = byte(s.IX & 0x00ff)
},
// 0x7e : LD A, (IX+n)
0x7E: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.A = s.core.MemRead(offset)
},
// 0x84 : ADD A, IXH (Undocumented)
0x84: func(s *Z80Type) {
s.doAdd(byte(s.IX >> 8))
},
// 0x85 : ADD A, IXL (Undocumented)
0x85: func(s *Z80Type) {
s.doAdd(byte(s.IX & 0x00ff))
},
// 0x86 : ADD A, (IX+n)
0x86: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.doAdd(s.core.MemRead(offset))
},
// 0x8c : ADC A, IXH (Undocumented)
0x8C: func(s *Z80Type) {
s.doAdc(byte(s.IX >> 8))
},
// 0x8d : ADC A, IXL (Undocumented)
0x8D: func(s *Z80Type) {
s.doAdc(byte(s.IX & 0x00ff))
},
// 0x8e : ADC A, (IX+n)
0x8E: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.doAdc(s.core.MemRead(offset))
},
// 0x94 : SUB IXH (Undocumented)
0x94: func(s *Z80Type) {
s.doSub(byte(s.IX >> 8))
},
// 0x95 : SUB IXL (Undocumented)
0x95: func(s *Z80Type) {
s.doSub(byte(s.IX & 0x00ff))
},
// 0x96 : SUB A, (IX+n)
0x96: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.doSub(s.core.MemRead(offset))
},
// 0x9c : SBC IXH (Undocumented)
0x9C: func(s *Z80Type) {
s.doSbc(byte(s.IX >> 8))
},
// 0x9d : SBC IXL (Undocumented)
0x9D: func(s *Z80Type) {
s.doSbc(byte(s.IX & 0x00ff))
},
// 0x9e : SBC A, (IX+n)
0x9E: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.doSbc(s.core.MemRead(offset))
},
// 0xa4 : AND IXH (Undocumented)
0xA4: func(s *Z80Type) {
s.doAnd(byte(s.IX >> 8))
},
// 0xa5 : AND IXL (Undocumented)
0xA5: func(s *Z80Type) {
s.doAnd(byte(s.IX & 0x00ff))
},
// 0xa6 : AND A, (IX+n)
0xA6: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.doAnd(s.core.MemRead(offset))
},
// 0xac : XOR IXH (Undocumented)
0xAC: func(s *Z80Type) {
s.doXor(byte(s.IX >> 8))
},
// 0xad : XOR IXL (Undocumented)
0xAD: func(s *Z80Type) {
s.doXor(byte(s.IX & 0x00ff))
},
// 0xae : XOR A, (IX+n)
0xAE: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.doXor(s.core.MemRead(offset))
},
// 0xb4 : OR IXH (Undocumented)
0xB4: func(s *Z80Type) {
s.doOr(byte(s.IX >> 8))
},
// 0xb5 : OR IXL (Undocumented)
0xB5: func(s *Z80Type) {
s.doOr(byte(s.IX & 0x00ff))
},
// 0xb6 : OR A, (IX+n)
0xB6: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.doOr(s.core.MemRead(offset))
},
// 0xbc : CP IXH (Undocumented)
0xBC: func(s *Z80Type) {
s.doCp(byte(s.IX >> 8))
},
// 0xbd : CP IXL (Undocumented)
0xBD: func(s *Z80Type) {
s.doCp(byte(s.IX & 0x00ff))
},
// 0xbe : CP A, (IX+n)
0xBE: func(s *Z80Type) {
offset := s.getOffset(s.IX)
s.doCp(s.core.MemRead(offset))
},
// 0xcb : CB Prefix (IX bit instructions)
0xCB: func(s *Z80Type) {
s.opcodeDDCB()
},
// 0xe1 : POP IX
0xE1: func(s *Z80Type) {
s.IX = s.PopWord()
},
// 0xe3 : EX (SP), IX
0xE3: func(s *Z80Type) {
temp := s.IX
s.IX = uint16(s.core.MemRead(s.SP))
s.IX |= uint16(s.core.MemRead(s.SP+1)) << 8
s.core.MemWrite(s.SP, byte(temp&0x00ff))
s.core.MemWrite(s.SP+1, byte(temp>>8))
},
// 0xe5 : PUSH IX
0xE5: func(s *Z80Type) {
s.pushWord(s.IX)
},
// 0xe9 : JP (IX)
0xE9: func(s *Z80Type) {
s.PC = s.IX - 1
},
// 0xf9 : LD SP, IX
0xf9: func(s *Z80Type) {
s.SP = s.IX
},
}
// =====================================================
func (z *Z80Type) getOffset(reg uint16) uint16 {
z.PC++
offset := z.core.MemRead(z.PC)
if offset < 0 {
reg -= uint16(-offset)
} else {
reg += uint16(offset)
}
return reg
}
func (z *Z80Type) opcodeDD() {
z.R = (z.R & 0x80) | (((z.R & 0x7f) + 1) & 0x7f)
z.PC++
opcode := z.core.M1MemRead(z.PC)
fun := ddInstructions[opcode]
if fun != nil {
//func = func.bind(this);
fun(z)
z.CycleCounter += CycleCountsDd[opcode]
} else {
// Apparently if a DD opcode doesn't exist,
// it gets treated as an unprefixed opcode.
// What we'll do to handle that is just back up the
// program counter, so that this byte gets decoded
// as a normal instruction.
z.PC--
// And we'll add in the cycle count for a NOP.
z.CycleCounter += CycleCounts[0]
}
}
func (z *Z80Type) opcodeDDCB() {
offset := z.getOffset(z.IX)
z.PC++
opcode := z.core.M1MemRead(z.PC)
value := byte(0)
bitTestOp := false
// As with the "normal" CB prefix, we implement the DDCB prefix
// by decoding the opcode directly, rather than using a table.
if opcode < 0x40 {
// Shift and rotate instructions.
ddcbFunctions := []OpShift{z.doRlc, z.doRrc, z.doRl, z.doRr, z.doSla, z.doSra, z.doSll, z.doSrl}
// Most of the opcodes in this range are not valid,
// so we map this opcode onto one of the ones that is.
fun := ddcbFunctions[(opcode&0x38)>>3]
value = fun(z.core.MemRead(offset))
z.core.MemWrite(offset, value)
} else {
bitNumber := (opcode & 0x38) >> 3
if opcode < 0x80 {
// BIT
bitTestOp = true
z.Flags.N = false
z.Flags.H = true
z.Flags.Z = z.core.MemRead(offset)&(1<<bitNumber) == 0
z.Flags.P = z.Flags.Z
z.Flags.S = (bitNumber == 7) && !z.Flags.Z
} else if opcode < 0xc0 {
// RES
value = z.core.MemRead(offset) & ^(1 << bitNumber)
z.core.MemWrite(offset, value)
} else {
// SET
value = z.core.MemRead(offset | (1 << bitNumber))
z.core.MemWrite(offset, value)
}
}
// This implements the undocumented shift, RES, and SET opcodes,
// which write their result to memory and also to an 8080 register.
if !bitTestOp {
value := byte(1)
switch opcode & 0x07 {
case 0:
z.B = value
case 1:
z.C = value
case 2:
z.D = value
case 3:
z.E = value
case 4:
z.H = value
case 5:
z.L = value
// 6 is the documented opcode, which doesn't set a register.
case 7:
z.A = value
}
}
z.CycleCounter += CycleCountsCb[opcode] + 8
}

Some files were not shown because too many files have changed in this diff Show More