commit c4536f7e051e8cf8e72e569cfcc1fe8384995094 Author: wehub-resource-sync Date: Mon Jul 13 12:30:31 2026 +0800 chore: import upstream snapshot with attribution diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml new file mode 100644 index 0000000..67d07cb --- /dev/null +++ b/.github/workflows/go.yml @@ -0,0 +1,66 @@ +name: CI + +on: + push: + branches: [main] + tags: ["v*"] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + - run: sudo apt-get update && sudo apt-get install -y gcc pkg-config libwayland-dev libx11-dev libx11-xcb-dev libxkbcommon-x11-dev libgles2-mesa-dev libegl1-mesa-dev libffi-dev libxcursor-dev libvulkan-dev + - run: go test ./... + + build-macos: + if: github.event_name == 'workflow_dispatch' || (github.ref_type == 'tag' && startsWith(github.ref_name, 'v')) + needs: test + name: macOS ${{ matrix.goarch }} + strategy: + fail-fast: false + matrix: + include: + - runner: macos-15 + goarch: arm64 + - runner: macos-15-intel + goarch: amd64 + runs-on: ${{ matrix.runner }} + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-go@v6 + with: + go-version-file: go.mod + cache: true + - name: Build application DMG + shell: bash + run: | + if [[ "$GITHUB_REF_TYPE" == "tag" && "$GITHUB_REF_NAME" == v* ]]; then + # CFBundleShortVersionString accepts only digits and dots; + # strip any pre-release/build suffix (v1.2.3-rc1 -> 1.2.3). + version="${GITHUB_REF_NAME#v}" + version="${version%%-*}" + version="${version%%+*}" + else + version="0.0.${GITHUB_RUN_NUMBER}" + fi + ./scripts/build-macos-dmg.sh "$version" "$GITHUB_RUN_NUMBER" "${{ matrix.goarch }}" + - name: Upload DMG + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + # Artifact names are immutable in upload-artifact v4+; without a + # per-arch name both matrix jobs upload to "artifact" and the + # second one fails. + name: lensm-dmg-${{ matrix.goarch }} + path: dist/*.dmg + archive: false + if-no-files-found: error diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94fb43c --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib +data/arm64/ +data/x86/ + +# Test binary, built with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Dependency directories (remove the comment below to include it) +# vendor/ +.idea/ + +lensm +/output/ +/dist/ +/tmp/ +*.lensm-comments.json diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8d57f72 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2022 Egon Elbre + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..71cb8f8 --- /dev/null +++ b/README.md @@ -0,0 +1,79 @@ +# lensm + +A tool for viewing assembly and source. + +![Screenshot](./screenshot.gif) + +Install with the usual Go commands: + +``` +go install loov.dev/lensm@main +``` + +_For Linux you may need to add some [additional dependencies](https://gioui.org/doc/install/linux). You can use `go install --tags nowayland loov.dev/lensm@main` or `go install --tags nox11 loov.dev/lensm@main` respectively to skip building Wayland or X11 version._ + +Build this checkout locally: + +``` +go build -o lensm . +``` + +GitHub Actions also builds installable macOS disk images for Apple Silicon +and Intel. Download the DMG artifact from a successful CI run, open it, and +drag `Lensm.app` to Applications. CI builds use an ad-hoc signature, so macOS +may require the first launch through **Control-click → Open**. + +To build the DMG locally on macOS: + +``` +./scripts/build-macos-dmg.sh 0.0.0 1 "$(go env GOARCH)" +``` + +On Linux, use the same Gio tags if you want to skip a windowing backend: + +``` +go build -tags nowayland -o lensm . +go build -tags nox11 -o lensm . +``` + +Run the program with an executable path, or start it without arguments +and load an executable from the top bar. On macOS, use Choose... to select +the executable with the native Finder file dialog. The function filter is +available inside the UI. `-watch` allows to automatically reload the +executable and information when it changes. + +``` +lensm -watch lensm +``` + +Inside the code view: + +- follow call targets and use `Alt+Left/Right` (or `Cmd/Ctrl+[` and + `Cmd/Ctrl+]`) to navigate between functions; +- hover an assembly instruction to see its reference and a simplified + explanation when Lensm has a matching rule; +- drag across Go assembly, native assembly, or source lines to select a block, + then use `Cmd/Ctrl+C` to copy it. `Shift` extends a selection and + `Escape` clears it. + +Run lensm as an MCP server over stdio: + +``` +lensm mcp [-comments ./lensm.lensm-comments.json] ./lensm +``` + +The MCP server exposes tools for listing functions, reading a function's +Go source, Go assembly and native assembly, and reading or writing comments. +By default comments are stored in a sidecar file named +`.lensm-comments.json`. + +When the GUI and a separate `lensm mcp` process write the same sidecar, +saves merge per comment: each process's additions, edits, and deletions +survive, and only conflicting edits to the same comment resolve to the +last writer. + +Note: The program requires a binary that is built on your computer, otherwise the source code for the functions cannot be loaded. + +## Why? + +I wrote a blog post at https://www.storj.io/blog/lensm on why and how the core functionality works. diff --git a/README.wehub.md b/README.wehub.md new file mode 100644 index 0000000..cc2bbbf --- /dev/null +++ b/README.wehub.md @@ -0,0 +1,7 @@ +# WeHub 来源说明 + +- 原始项目:`loov/lensm` +- 原始仓库:https://github.com/loov/lensm +- 导入方式:上游默认分支的最新快照 +- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准 +- 本文件仅用于记录来源,不代表 WeHub 是原项目作者 diff --git a/data/download.sh b/data/download.sh new file mode 100755 index 0000000..f3ecc97 --- /dev/null +++ b/data/download.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash +# +# Downloads the CPU ISA source dumps that internal/asmref/gen parses into +# table.json. Both files are large and gitignored (see .gitignore); this script +# fetches them into .data/ so the generator can be pointed at them. +# +# Usage: +# .data/download.sh # download anything missing +# FORCE=1 .data/download.sh # re-download even if present +# +# After it finishes it prints the exact `go run ./internal/asmref/gen ...` +# command with the resolved paths. +set -euo pipefail + +DATA_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FORCE="${FORCE:-0}" + +# x86: uops.info (XED-derived) instruction + port data. ~140MB. +X86_URL="https://uops.info/instructions.xml" +X86_FILE="$DATA_DIR/x86/instructions.xml" + +# arm64: ARM's official A64 ISA XML. URL and version are pinned to match the +# release golang.org/x/arch/arm64/instgen/xmlspec expects (its ExpectedURL). +ARM_VERSION="ISA_A64_xml_A_profile-2025-12" +ARM_URL="https://developer.arm.com/-/cdn-downloads/permalink/Exploration-Tools-A64-ISA/ISA_A64/${ARM_VERSION}.tar.gz" +ARM_TARBALL="$DATA_DIR/arm64/${ARM_VERSION}.tar.gz" +ARM_DIR="$DATA_DIR/arm64/${ARM_VERSION}" + +fetch() { # url dest + echo " downloading $1" + curl -fL --progress-bar -o "$2" "$1" +} + +echo "==> x86 (uops.info)" +if [[ "$FORCE" != "1" && -s "$X86_FILE" ]]; then + echo " present, skipping: $X86_FILE" +else + mkdir -p "$(dirname "$X86_FILE")" + fetch "$X86_URL" "$X86_FILE" +fi + +echo "==> arm64 (ARM A64 ISA XML, ${ARM_VERSION})" +if [[ "$FORCE" != "1" && -d "$ARM_DIR" ]]; then + echo " present, skipping: $ARM_DIR" +else + mkdir -p "$DATA_DIR/arm64" + fetch "$ARM_URL" "$ARM_TARBALL" + echo " extracting" + tar xzf "$ARM_TARBALL" -C "$DATA_DIR/arm64" + rm -f "$ARM_TARBALL" + xml_count=$(find "$ARM_DIR" -name '*.xml' | wc -l | tr -d ' ') + echo " extracted ${xml_count} xml files" +fi + +echo +echo "Sources ready. Generate the real table with:" +echo +echo " go run ./internal/asmref/gen \\" +echo " -arm $ARM_DIR \\" +echo " -x86 $X86_FILE \\" +echo " -x86arch ADL-P \\" +echo " -out internal/asmref/table.json" diff --git a/debug.go b/debug.go new file mode 100644 index 0000000..9a38104 --- /dev/null +++ b/debug.go @@ -0,0 +1,12 @@ +package main + +const N = 44 + +func Fibonacci(n int) int { + if n <= 1 { + return n + } + return Fibonacci(n-1) + Fibonacci(n-2) +} + +var _ = Fibonacci(0) diff --git a/fileui.go b/fileui.go new file mode 100644 index 0000000..81763bb --- /dev/null +++ b/fileui.go @@ -0,0 +1,785 @@ +package main + +import ( + "errors" + "fmt" + "image" + "io" + "os" + "slices" + "strconv" + "strings" + "time" + + "gioui.org/app" + "gioui.org/font" + "gioui.org/io/clipboard" + "gioui.org/io/event" + "gioui.org/io/key" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" + "gioui.org/x/component" + "gioui.org/x/explorer" + + "loov.dev/lensm/internal/codeview" + "loov.dev/lensm/internal/comments" + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/gui" + "loov.dev/lensm/internal/mcp" + "loov.dev/lensm/internal/syntax" +) + +var workInProgressWASM bool + +type FileUIConfig struct { + Path string + Watch bool + Context int + CommentsPath string +} + +type FileUI struct { + Windows *gui.Windows + Theme *gui.Theme + + Config FileUIConfig + Settings AppSettings + + LoadError error + + // Currently loaded executable. + File disasm.File + Funcs *gui.FilterList[disasm.Func] + + // Active code view. + CodeTabs []*CodeTab + ActiveTab int + Tabs widget.List + + // split divides the function list from the code view. + split component.Resize + + // Other FileUI elements. + BrowseButton widget.Clickable + SettingsButton widget.Clickable + Dark widget.Bool + SyntaxStyle widget.Enum + ShowNativeAsm widget.Bool + ShowAsmHelp widget.Bool + Comment widget.Editor + TextSizeEditor widget.Editor + StartMCP widget.Clickable + StopMCP widget.Clickable + + Comments *comments.Store + MCP *mcp.AppServer + + picker *explorer.Explorer + loader *loader + invalidate chan struct{} + settingsEvents chan event.Event + settingsAcks chan struct{} + pickerResults chan pickerResult + exited chan struct{} + flushTimer *time.Timer + sessionDirty bool + commentKey string + copyStatus string + saveError string + settingsWindowOpen bool + pickerOpen bool + loadGeneration uint64 + loadedPath string + Navigation NavigationHistory + navigatingHistory bool +} + +type pickerResult struct { + path string + ok bool + err error +} + +func NewFileUI(windows *gui.Windows, theme *material.Theme) *FileUI { + ui := &FileUI{} + ui.Windows = windows + settings, err := LoadAppSettings() + if err != nil { + fmt.Fprintf(os.Stderr, "unable to load settings: %v\n", err) + } + ui.Settings = settings + ui.Theme = gui.NewTheme(theme, settings.Dark) + ui.SyntaxStyle.Value = settings.SyntaxStyle + ui.Dark.Value = settings.Dark + ui.Funcs = gui.NewFilterList[disasm.Func](ui.Theme) + ui.ActiveTab = -1 + ui.Navigation.Reset() + ui.Tabs.List.Axis = layout.Horizontal + ui.ShowNativeAsm.Value = settings.ShowNativeAsm + ui.ShowAsmHelp.Value = settings.ShowAsmHelp + ui.split.Axis = layout.Horizontal + ui.split.Ratio = settings.SidebarRatio + ui.Comment.SingleLine = true + ui.TextSizeEditor.SingleLine = true + ui.TextSizeEditor.Submit = true + ui.TextSizeEditor.SetText(strconv.Itoa(settings.TextSize)) + ui.Comments, _ = comments.Open("", "") + return ui +} + +func (ui *FileUI) Run(w *app.Window) error { + var ops op.Ops + + exited := make(chan struct{}) + defer close(exited) + defer func() { + if ui.MCP != nil { + _ = ui.MCP.Close() + } + }() + + loader := newLoader(loadDisasmFile, ui.Config.Watch) + ui.loader = loader + defer loader.Close() + picker := explorer.NewExplorer(w) + ui.picker = picker + invalidate := make(chan struct{}, 1) + ui.invalidate = invalidate + settingsEvents := make(chan event.Event) + settingsAcks := make(chan struct{}) + ui.settingsEvents = settingsEvents + ui.settingsAcks = settingsAcks + pickerResults := make(chan pickerResult, 1) + ui.pickerResults = pickerResults + ui.exited = exited + flushTimer := time.NewTimer(time.Hour) + flushTimer.Stop() + ui.flushTimer = flushTimer + var settingsOps op.Ops + defer ui.flushPending() + defer func() { + ui.picker = nil + ui.loader = nil + ui.invalidate = nil + ui.settingsEvents = nil + ui.settingsAcks = nil + ui.pickerResults = nil + ui.exited = nil + ui.flushTimer = nil + }() + + if ui.Config.Path != "" { + ui.requestLoad(ui.Config.Path) + } + + events := make(chan event.Event) + acks := make(chan struct{}) + + go func() { + for { + ev := w.Event() + events <- ev + <-acks + if _, ok := ev.(app.DestroyEvent); ok { + return + } + } + }() + + for { + select { + case result := <-loader.Results(): + if result.generation != ui.loadGeneration { + if result.file != nil { + _ = result.file.Close() + } + continue + } + if result.err != nil { + ui.LoadError = result.err + if ui.MCP != nil { + ui.MCP.SetPath("", ui.Comments) + } + w.Invalidate() + continue + } + ui.SetFile(result.file) + w.Invalidate() + case <-invalidate: + w.Invalidate() + case <-flushTimer.C: + ui.flushPending() + w.Invalidate() + case res := <-pickerResults: + ui.pickerOpen = false + if res.err != nil { + ui.LoadError = res.err + } else if res.ok { + ui.Config.Path = res.path + ui.LoadError = nil + ui.copyStatus = "" + ui.requestLoad(res.path) + } + w.Invalidate() + case ev := <-settingsEvents: + switch e := ev.(type) { + case app.FrameEvent: + gtx := app.NewContext(&settingsOps, e) + ui.layoutSettingsWindow(gtx) + e.Frame(gtx.Ops) + case app.DestroyEvent: + ui.settingsWindowOpen = false + } + settingsAcks <- struct{}{} + case e := <-events: + picker.ListenEvents(e) + switch e := e.(type) { + case app.FrameEvent: + gtx := app.NewContext(&ops, e) + ui.Layout(gtx) + e.Frame(gtx.Ops) + + case app.DestroyEvent: + acks <- struct{}{} + return e.Err + } + acks <- struct{}{} + } + } +} + +func (ui *FileUI) SetFile(file disasm.File) { + ui.navigatingHistory = true + defer func() { + ui.navigatingHistory = false + if tab := ui.activeTab(); tab != nil { + ui.Navigation.Visit(tab.Name) + } + }() + // Keep history across watch-mode reloads of the same binary; open + // tabs are preserved there too. Entries whose functions vanished are + // skipped during navigation. + path := comments.CleanPath(ui.Config.Path) + if ui.File == nil || path != ui.loadedPath { + ui.Navigation.Reset() + } + ui.loadedPath = path + + initialLoad := ui.File == nil + if ui.File != nil { + _ = ui.File.Close() + } + + openNames := make([]string, 0, len(ui.CodeTabs)) + for _, tab := range ui.CodeTabs { + openNames = append(openNames, tab.Name) + } + activeName := ui.Funcs.Selected + if tab := ui.activeTab(); tab != nil { + activeName = tab.Name + } + if initialLoad && comments.CleanPath(ui.Config.Path) == ui.Settings.LastPath && len(ui.Settings.OpenTabs) > 0 { + openNames = append(openNames[:0], ui.Settings.OpenTabs...) + activeName = ui.Settings.ActiveTab + if activeName == "" && len(openNames) > 0 { + activeName = openNames[0] + } + } + + ui.File = file + ui.LoadError = nil + ui.loadCommentsForPath(ui.Config.Path) + ui.CodeTabs = nil + ui.ActiveTab = -1 + ui.commentKey = "" + ui.Funcs.SetItems(file.Funcs()) + + if activeName != "" { + ui.selectFuncByName(activeName) + } + for _, name := range openNames { + fn := ui.findFunc(name) + if fn == nil { + continue + } + tab := ui.appendCodeTab(fn) + if name == activeName { + ui.ActiveTab = len(ui.CodeTabs) - 1 + ui.selectFuncByName(tab.Name) + } + } + + if len(ui.CodeTabs) == 0 { + if ui.Funcs.SelectedItem == nil && len(ui.Funcs.Filtered) > 0 { + ui.Funcs.SelectIndex(0) + } + ui.openTab(ui.Funcs.SelectedItem, false) + ui.afterFileLoaded() + return + } + if !gui.InRange(ui.ActiveTab, len(ui.CodeTabs)) { + ui.ActiveTab = 0 + ui.selectFuncByName(ui.CodeTabs[ui.ActiveTab].Name) + } + ui.afterFileLoaded() +} + +func (ui *FileUI) loadOptions() disasm.Options { + return disasm.Options{Context: ui.Config.Context} +} + +func (ui *FileUI) findFunc(name string) disasm.Func { + if ui.File == nil { + return nil + } + for _, fn := range ui.File.Funcs() { + if fn.Name() == name { + return fn + } + } + return nil +} + +func (ui *FileUI) selectFuncByName(name string) { + ui.Funcs.Selected = name + ui.Funcs.List.Selected = -1 + var zero disasm.Func + ui.Funcs.SelectedItem = zero + + for i, fn := range ui.Funcs.Filtered { + if fn.Name() == name { + ui.Funcs.List.Selected = i + ui.Funcs.SelectedItem = fn + return + } + } + for _, fn := range ui.Funcs.All { + if fn.Name() == name { + ui.Funcs.SelectedItem = fn + return + } + } +} + +func (ui *FileUI) Layout(gtx layout.Context) { + colors := ui.Theme.Colors + paint.FillShape(gtx.Ops, colors.Background, clip.Rect{Max: gtx.Constraints.Max}.Op()) + + event.Op(gtx.Ops, ui) + ui.handleActions(gtx) + + layout.Flex{ + Axis: layout.Vertical, + }.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return ui.layoutToolbar(gtx, colors) + }), + layout.Rigid(gui.HorizontalLine{Height: 1, Color: colors.Splitter}.Layout), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return ui.layoutContent(gtx, colors) + }), + ) +} + +func (ui *FileUI) handleActions(gtx layout.Context) { + // widget.Editor claims Option+arrow word-jumps on macOS, and this + // unfocused poll runs before any editor's Update, so it would steal + // them from a focused editor. Only listen for Alt+arrows while no + // text editor has focus. + editorFocused := gtx.Focused(&ui.Comment) || gtx.Focused(&ui.Funcs.Filter) + filters := []event.Filter{ + key.Filter{Required: key.ModShortcut, Name: key.Name("[")}, + key.Filter{Required: key.ModShortcut, Name: key.Name("]")}, + key.Filter{Required: key.ModShortcut, Name: key.Name("W")}, + } + if !editorFocused { + filters = append(filters, + key.Filter{Required: key.ModAlt, Name: key.NameLeftArrow}, + key.Filter{Required: key.ModAlt, Name: key.NameRightArrow}, + ) + } + for { + ev, ok := gtx.Event(filters...) + if !ok { + break + } + keyEvent, ok := ev.(key.Event) + if !ok || keyEvent.State != key.Press { + continue + } + switch keyEvent.Name { + case key.NameLeftArrow, key.Name("["): + ui.navigateBack() + case key.NameRightArrow, key.Name("]"): + ui.navigateForward() + case key.Name("W"): + ui.closeTab(ui.ActiveTab) + } + } + for ui.BrowseButton.Clicked(gtx) { + ui.chooseFile() + } + for ui.SettingsButton.Clicked(gtx) { + ui.openSettingsWindow() + } +} + +func (ui *FileUI) layoutToolbar(gtx layout.Context, colors gui.UIColors) layout.Dimensions { + paint.FillShape(gtx.Ops, colors.SecondaryBackground, clip.Rect{Max: gtx.Constraints.Max}.Op()) + + inset := layout.Inset{Top: 4, Right: 6, Bottom: 4, Left: 6} + return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + button := material.Button(ui.Theme.Theme, &ui.BrowseButton, "Choose...") + button.Inset = layout.Inset{Top: 6, Right: 10, Bottom: 6, Left: 10} + return layout.Inset{Right: 6}.Layout(gtx, button.Layout) + }), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + if ui.Config.Path == "" { + return layout.Dimensions{Size: image.Pt(gtx.Constraints.Max.X, 0)} + } + label := ui.Theme.Muted(ui.Config.Path, 0.8) + label.MaxLines = 1 + return layout.W.Layout(gtx, label.Layout) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + button := material.IconButton(ui.Theme.Theme, &ui.SettingsButton, SettingsIcon, "Settings") + button.Size = 18 + button.Inset = layout.UniformInset(8) + return layout.Inset{Left: 4}.Layout(gtx, button.Layout) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if ui.copyStatus == "" { + return layout.Dimensions{} + } + label := ui.Theme.Muted(ui.copyStatus, 0.75) + return layout.Inset{Left: 2}.Layout(gtx, label.Layout) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if ui.saveError == "" { + return layout.Dimensions{} + } + label := ui.Theme.ErrorLabel(ui.saveError, 0.75) + return layout.Inset{Left: 2}.Layout(gtx, label.Layout) + }), + ) + }) +} + +func (ui *FileUI) layoutSyntaxSelector(gtx layout.Context, colors gui.UIColors) layout.Dimensions { + return layout.Inset{Left: 10}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := ui.Theme.Muted("Syntax", 0.85) + return layout.Inset{Right: 3}.Layout(gtx, label.Layout) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return ui.layoutSyntaxRadio(gtx, colors, syntax.StyleGoLand) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return ui.layoutSyntaxRadio(gtx, colors, syntax.StyleDarcula) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return ui.layoutSyntaxRadio(gtx, colors, syntax.StyleMono) + }), + ) + }) +} + +func (ui *FileUI) layoutSyntaxRadio(gtx layout.Context, colors gui.UIColors, style string) layout.Dimensions { + radio := material.RadioButton(ui.Theme.Theme, &ui.SyntaxStyle, style, syntax.StyleLabel(style)) + radio.Color = colors.MutedText + radio.IconColor = ui.Theme.ContrastBg + radio.TextSize = ui.Theme.TextSize * 0.78 + radio.Size = unit.Dp(18) + return layout.Inset{Left: 2}.Layout(gtx, radio.Layout) +} + +// scheduleFlush arranges for buffered comment and settings changes to +// reach disk once the user pauses, instead of on every keystroke or tab +// switch. Main event loop only. +func (ui *FileUI) scheduleFlush() { + if ui.flushTimer == nil { + ui.flushPending() + return + } + ui.flushTimer.Reset(time.Second) +} + +func (ui *FileUI) flushPending() { + ui.saveError = "" + if ui.sessionDirty { + if err := SaveAppSettings(ui.Settings); err != nil { + ui.saveError = "settings not saved: " + err.Error() + fmt.Fprintf(os.Stderr, "unable to save settings: %v\n", err) + } else { + ui.sessionDirty = false + } + } + if err := ui.Comments.Flush(); err != nil { + ui.saveError = "comments not saved: " + err.Error() + fmt.Fprintf(os.Stderr, "unable to save comments: %v\n", err) + } +} + +func (ui *FileUI) invalidateMain() { + if ui.invalidate == nil { + return + } + select { + case ui.invalidate <- struct{}{}: + default: + } +} + +func (ui *FileUI) chooseFile() { + if ui.pickerOpen || ui.picker == nil || ui.pickerResults == nil { + return + } + // The native picker blocks until dismissed; running it here would + // stall the frame in progress and freeze the window. + ui.pickerOpen = true + picker, results, exited := ui.picker, ui.pickerResults, ui.exited + go func() { + var res pickerResult + // No extension filter: Go executables usually have none. + file, err := picker.ChooseFile() + switch { + case err == nil: + // The disassembler needs a path, not a reader; on desktop + // platforms the explorer hands back an *os.File. + if f, ok := file.(*os.File); ok { + res = pickerResult{path: f.Name(), ok: true} + } else { + res.err = errors.New("file picker did not return a local file path") + } + _ = file.Close() + case errors.Is(err, explorer.ErrUserDecline): + default: + res.err = err + } + select { + case results <- res: + case <-exited: + } + }() +} + +func (ui *FileUI) layoutContent(gtx layout.Context, colors gui.UIColors) layout.Dimensions { + active := ui.activeCode() + if active == nil || !active.Loaded() || active.Name != ui.Funcs.Selected { + selected := ui.Funcs.SelectedItem + if selected != nil { + ui.previewTab(selected) + } + } + + // Keep both panes usable at any window size; the ratio from a drag + // or from settings is clamped, not trusted. + total := float32(gtx.Constraints.Max.X) + minSidebar := float32(gtx.Metric.Dp(120)) + minContent := float32(gtx.Metric.Dp(240)) + ui.split.Ratio = min(max(ui.split.Ratio, minSidebar/total), 1-minContent/total) + + defer func() { + if ui.split.Ratio != ui.Settings.SidebarRatio { + settings := ui.Settings + settings.SidebarRatio = ui.split.Ratio + ui.saveSettings(settings) + } + }() + + return ui.split.Layout(gtx, + func(gtx layout.Context) layout.Dimensions { + gtx.Constraints = layout.Exact(gtx.Constraints.Max) + return ui.Funcs.Layout(ui.Theme, gtx) + }, + func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if ui.LoadError != nil { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + txt := ui.Theme.ErrorLabel(ui.LoadError.Error(), 1) + return layout.UniformInset(6).Layout(gtx, txt.Layout) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if ui.File == nil { + return layout.Dimensions{} + } + return ui.layoutCodeTabs(gtx, colors) + }), + ) + } + return ui.layoutCodeTabs(gtx, colors) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + code := ui.activeCode() + if code == nil || !code.Loaded() { + return layout.Dimensions{} + } + txt := ui.Theme.Muted("file: "+code.Code.File, 1) + txt.Font.Style = font.Italic + + inset := layout.Inset{Top: 2, Left: 4, Right: 4, Bottom: 4} + return inset.Layout(gtx, txt.Layout) + }), + layout.Rigid(gui.HorizontalLine{Height: 1, Color: colors.Splitter}.Layout), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + if ui.LoadError != nil && ui.File == nil { + return layout.Dimensions{} + } + code := ui.activeCode() + if code == nil { + return layout.Dimensions{} + } + + gtx.Constraints = layout.Exact(gtx.Constraints.Max) + return layout.Stack{ + Alignment: layout.SE, + }.Layout(gtx, + layout.Expanded(func(gtx layout.Context) layout.Dimensions { + return codeview.Style{ + UI: code, + + TryOpen: ui.tryOpen, + OnInteract: ui.keepActiveTab, + CopyText: func(gtx layout.Context, text string) { + ui.writeClipboardText(gtx, text, "Copied selection") + }, + + Comments: ui.Comments, + SetComment: ui.setBufferedComment, + CommentKey: &ui.commentKey, + CommentEditor: &ui.Comment, + + Theme: ui.Theme, + Syntax: syntax.PaletteFor(ui.Settings.SyntaxStyle, colors.SyntaxColors()), + ShowNative: ui.ShowNativeAsm.Value, + ShowHelp: ui.ShowAsmHelp.Value, + TextHeight: ui.Theme.TextSize, + }.Layout(gtx) + }), + ) + }), + ) + }, + func(gtx layout.Context) layout.Dimensions { + size := image.Pt(gtx.Metric.Dp(8), gtx.Constraints.Max.Y) + paint.FillShape(gtx.Ops, colors.Splitter, clip.Rect{ + Min: image.Pt(size.X/2, 0), + Max: image.Pt(size.X/2+1, size.Y), + }.Op()) + return layout.Dimensions{Size: size} + }, + ) +} + +func (ui *FileUI) requestLoad(path string) { + if ui.loader == nil { + return + } + ui.loadGeneration++ + ui.loader.Request(ui.loadGeneration, path) +} + +func (ui *FileUI) afterFileLoaded() { + ui.saveSessionState() + if ui.MCP != nil { + ui.MCP.SetPath(ui.Config.Path, ui.Comments) + } +} + +func (ui *FileUI) saveSessionState() { + if ui.File == nil { + return + } + path := comments.CleanPath(ui.Config.Path) + if path == "" { + return + } + + openTabs := make([]string, 0, len(ui.CodeTabs)) + for _, tab := range ui.CodeTabs { + if tab.Name != "" && !tab.Preview { + openTabs = append(openTabs, tab.Name) + } + } + activeTab := "" + if tab := ui.activeTab(); tab != nil { + activeTab = tab.Name + } + + settings := ui.Settings + settings.LastPath = path + settings.OpenTabs = cleanFuncNames(openTabs) + settings.ActiveTab = activeTab + if settings.ActiveTab != "" && !slices.Contains(settings.OpenTabs, settings.ActiveTab) { + settings.ActiveTab = "" + } + if ui.Settings.LastPath == settings.LastPath && + slices.Equal(ui.Settings.OpenTabs, settings.OpenTabs) && + ui.Settings.ActiveTab == settings.ActiveTab { + return + } + + ui.Settings = settings + ui.sessionDirty = true + ui.scheduleFlush() +} + +func (ui *FileUI) writeClipboardText(gtx layout.Context, text, status string) { + if text == "" { + return + } + gtx.Execute(clipboard.WriteCmd{ + Type: "text/plain", + Data: io.NopCloser(strings.NewReader(text)), + }) + ui.copyStatus = status +} + +func (ui *FileUI) tryOpen(gtx layout.Context, call string) { + fn := ui.findFunc(call) + if fn == nil { + return + } + + ui.openTab(fn, true) + gtx.Execute(op.InvalidateCmd{}) +} + +func (ui *FileUI) startMCP() { + if ui.MCP != nil { + return + } + server, err := mcp.StartAppServer(loadDisasmFile, ui.Config.CommentsPath) + if err != nil { + ui.LoadError = fmt.Errorf("unable to start MCP server: %w", err) + ui.invalidateMain() + return + } + ui.MCP = server + if ui.File != nil { + ui.MCP.SetPath(ui.Config.Path, ui.Comments) + } + fmt.Fprintf(os.Stderr, "lensm MCP server listening at %s\n", server.URL()) + ui.invalidateMain() +} + +func (ui *FileUI) stopMCP() { + if ui.MCP == nil { + return + } + if err := ui.MCP.Close(); err != nil { + fmt.Fprintf(os.Stderr, "unable to stop MCP server: %v\n", err) + } + ui.MCP = nil + ui.invalidateMain() +} diff --git a/fileui_comments.go b/fileui_comments.go new file mode 100644 index 0000000..cc0eb1c --- /dev/null +++ b/fileui_comments.go @@ -0,0 +1,36 @@ +package main + +import ( + "fmt" + "os" + + "loov.dev/lensm/internal/comments" +) + +func (ui *FileUI) loadCommentsForPath(exePath string) { + // Write out anything buffered for the previous binary first. + if err := ui.Comments.Flush(); err != nil { + fmt.Fprintf(os.Stderr, "unable to save comments: %v\n", err) + } + commentsPath := ui.Config.CommentsPath + if commentsPath == "" { + commentsPath = comments.DefaultPath(exePath) + } + store, err := comments.Open(commentsPath, exePath) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to load comments from %q: %v\n", commentsPath, err) + store, _ = comments.Open("", exePath) + } + ui.Comments = store +} + +// setBufferedComment records the comment in memory and schedules the +// disk write, so typing doesn't rewrite the sidecar per keystroke. +func (ui *FileUI) setBufferedComment(coord comments.Coord, text string) { + if err := ui.Comments.SetBuffered(coord, text); err != nil { + ui.saveError = "comment not saved: " + err.Error() + fmt.Fprintln(os.Stderr, err) + return + } + ui.scheduleFlush() +} diff --git a/fileui_navigation_test.go b/fileui_navigation_test.go new file mode 100644 index 0000000..244d856 --- /dev/null +++ b/fileui_navigation_test.go @@ -0,0 +1,102 @@ +package main + +import ( + "testing" + + "gioui.org/widget/material" + + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/gui" +) + +type navigationTestFunc string + +func (fn navigationTestFunc) Name() string { return string(fn) } +func (fn navigationTestFunc) Load(disasm.Options) (*disasm.Code, error) { + return &disasm.Code{Name: string(fn)}, nil +} + +type navigationTestFile struct{ funcs []disasm.Func } + +func (file navigationTestFile) Close() error { return nil } +func (file navigationTestFile) Funcs() []disasm.Func { return file.funcs } + +func TestFileUINavigationBackAndForward(t *testing.T) { + functions := []disasm.Func{ + navigationTestFunc("main.A"), + navigationTestFunc("main.B"), + navigationTestFunc("main.C"), + } + theme := gui.NewTheme(material.NewTheme(), false) + ui := &FileUI{ + Theme: theme, + File: navigationTestFile{funcs: functions}, + Funcs: gui.NewFilterList[disasm.Func](theme), + ActiveTab: -1, + } + ui.Navigation.Reset() + ui.Funcs.SetItems(functions) + + ui.openTab(functions[0], false) + ui.openTab(functions[1], true) + ui.openTab(functions[2], true) + if got := ui.Navigation.Current(); got != "main.C" { + t.Fatalf("current history entry = %q", got) + } + + ui.navigateBack() + if got := ui.activeTab().Name; got != "main.B" { + t.Fatalf("active tab after Back = %q", got) + } + ui.navigateForward() + if got := ui.activeTab().Name; got != "main.C" { + t.Fatalf("active tab after Forward = %q", got) + } +} + +func TestFileUIPreviewTab(t *testing.T) { + functions := []disasm.Func{ + navigationTestFunc("main.A"), + navigationTestFunc("main.B"), + navigationTestFunc("main.C"), + } + theme := gui.NewTheme(material.NewTheme(), false) + ui := &FileUI{ + Theme: theme, + File: navigationTestFile{funcs: functions}, + Funcs: gui.NewFilterList[disasm.Func](theme), + ActiveTab: -1, + } + ui.Navigation.Reset() + ui.Funcs.SetItems(functions) + + // Browsing the list only ever keeps a single preview tab. + ui.previewTab(functions[0]) + ui.previewTab(functions[1]) + ui.previewTab(functions[2]) + if len(ui.CodeTabs) != 1 { + t.Fatalf("preview browsing tabs = %d, want 1", len(ui.CodeTabs)) + } + if got := ui.activeTab().Name; got != "main.C" { + t.Fatalf("preview tab name = %q", got) + } + + // Clicking the tab keeps it; the next preview opens a second tab. + ui.selectTab(0) + if ui.CodeTabs[0].Preview { + t.Fatalf("tab still preview after selectTab") + } + ui.previewTab(functions[0]) + if len(ui.CodeTabs) != 2 { + t.Fatalf("tabs after keeping = %d, want 2", len(ui.CodeTabs)) + } + + // Acting on the preview content keeps it too. + if !ui.activeTab().Preview { + t.Fatalf("expected active tab to be preview") + } + ui.keepActiveTab() + if ui.activeTab().Preview { + t.Fatalf("keepActiveTab did not clear Preview") + } +} diff --git a/fileui_settings.go b/fileui_settings.go new file mode 100644 index 0000000..e2af8fa --- /dev/null +++ b/fileui_settings.go @@ -0,0 +1,229 @@ +package main + +import ( + "image" + "strconv" + "strings" + + "gioui.org/app" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" + + "loov.dev/lensm/internal/gui" + "loov.dev/lensm/internal/syntax" +) + +func (ui *FileUI) openSettingsWindow() { + if ui.settingsWindowOpen || ui.settingsEvents == nil { + return + } + ui.settingsWindowOpen = true + events, acks, exited := ui.settingsEvents, ui.settingsAcks, ui.exited + ui.Windows.Open("lensm settings", image.Pt(520, 360), func(w *app.Window) error { + // Only pump events here: the settings window is laid out on the + // main event loop, because layout reads and mutates state shared + // with the main window (Settings, MCP, Config, widget state) and + // shapes text through the Theme's Shaper, which is not safe for + // concurrent use. + for { + ev := w.Event() + select { + case events <- ev: + case <-exited: + return nil + } + select { + case <-acks: + case <-exited: + return nil + } + if e, ok := ev.(app.DestroyEvent); ok { + return e.Err + } + } + }) +} + +func (ui *FileUI) layoutSettingsWindow(gtx layout.Context) layout.Dimensions { + colors := ui.Theme.Colors + paint.FillShape(gtx.Ops, colors.Background, clip.Rect{Max: gtx.Constraints.Max}.Op()) + ui.handleSettingsActions(gtx) + + return layout.UniformInset(14).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + title := material.H6(ui.Theme.Theme, "Settings") + return layout.Inset{Bottom: 12}.Layout(gtx, title.Layout) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return ui.layoutSettingsSection(gtx, "Visual", []layout.FlexChild{ + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(material.Switch(ui.Theme.Theme, &ui.Dark, "Dark theme").Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := ui.Theme.Label("Dark theme", 1) + return layout.Inset{Left: 6}.Layout(gtx, label.Layout) + }), + ) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + check := material.CheckBox(ui.Theme.Theme, &ui.ShowNativeAsm, "Native asm") + return check.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + check := material.CheckBox(ui.Theme.Theme, &ui.ShowAsmHelp, "Show instruction help") + return check.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return ui.layoutSyntaxSelector(gtx, colors) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return ui.layoutLabeledEditor(gtx, "Text size", &ui.TextSizeEditor) + }), + }) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Inset{Top: 14}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return ui.layoutSettingsSection(gtx, "MCP", []layout.FlexChild{ + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + status := "Stopped" + if ui.MCP != nil { + status = "Running: " + ui.MCP.URL() + } + label := ui.Theme.Muted(status, 1) + return layout.Inset{Top: 6, Bottom: 6}.Layout(gtx, label.Layout) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if ui.MCP != nil { + gtx = gtx.Disabled() + } + button := material.Button(ui.Theme.Theme, &ui.StartMCP, "Start MCP") + return button.Layout(gtx) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if ui.MCP == nil { + gtx = gtx.Disabled() + } + button := material.Button(ui.Theme.Theme, &ui.StopMCP, "Stop MCP") + return layout.Inset{Left: 8}.Layout(gtx, button.Layout) + }), + ) + }), + }) + }) + }), + ) + }) +} + +func (ui *FileUI) layoutSettingsSection(gtx layout.Context, title string, children []layout.FlexChild) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + append([]layout.FlexChild{ + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := ui.Theme.Muted(title, 0.9) + return layout.Inset{Bottom: 6}.Layout(gtx, label.Layout) + }), + }, children...)..., + ) +} + +func (ui *FileUI) layoutLabeledEditor(gtx layout.Context, labelText string, editor *widget.Editor) layout.Dimensions { + return layout.Inset{Top: 6}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := ui.Theme.Label(labelText, 1) + return layout.Inset{Right: 8}.Layout(gtx, label.Layout) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + gtx.Constraints = layout.Exact(image.Pt(gtx.Metric.Dp(80), gtx.Metric.Dp(34))) + return gui.FocusBorder(ui.Theme.Theme, gtx.Focused(editor)).Layout(gtx, + material.Editor(ui.Theme.Theme, editor, "").Layout) + }), + ) + }) +} + +func (ui *FileUI) handleSettingsActions(gtx layout.Context) { + changedVisual := false + for ui.Dark.Update(gtx) { + ui.Theme.SetDark(ui.Dark.Value) + changedVisual = true + } + for ui.ShowNativeAsm.Update(gtx) { + changedVisual = true + } + for ui.ShowAsmHelp.Update(gtx) { + changedVisual = true + } + for ui.SyntaxStyle.Update(gtx) { + ui.SyntaxStyle.Value = syntax.NormalizeStyle(ui.SyntaxStyle.Value) + changedVisual = true + } + if ui.updatePositiveIntEditor(gtx, &ui.TextSizeEditor, func(value int) { + ui.Theme.TextSize = unit.Sp(value) + }) { + changedVisual = true + } + if changedVisual { + ui.saveVisualSettings() + gtx.Execute(op.InvalidateCmd{}) + ui.invalidateMain() + } + + for ui.StartMCP.Clicked(gtx) { + ui.startMCP() + gtx.Execute(op.InvalidateCmd{}) + } + for ui.StopMCP.Clicked(gtx) { + ui.stopMCP() + gtx.Execute(op.InvalidateCmd{}) + } +} + +func (ui *FileUI) updatePositiveIntEditor(gtx layout.Context, editor *widget.Editor, apply func(int)) bool { + changed := false + for { + ev, ok := editor.Update(gtx) + if !ok { + break + } + switch ev.(type) { + case widget.ChangeEvent, widget.SubmitEvent: + changed = true + } + } + if !changed { + return false + } + value, err := strconv.Atoi(strings.TrimSpace(editor.Text())) + if err != nil || value <= 0 { + return false + } + apply(value) + return true +} + +func (ui *FileUI) saveVisualSettings() { + settings := ui.Settings + settings.Dark = ui.Dark.Value + settings.ShowNativeAsm = ui.ShowNativeAsm.Value + settings.ShowAsmHelp = ui.ShowAsmHelp.Value + settings.SyntaxStyle = syntax.NormalizeStyle(ui.SyntaxStyle.Value) + if value, err := strconv.Atoi(strings.TrimSpace(ui.TextSizeEditor.Text())); err == nil && value > 0 { + settings.TextSize = value + } + ui.saveSettings(settings) +} + +func (ui *FileUI) saveSettings(settings AppSettings) { + ui.Settings = settings + ui.sessionDirty = true + ui.scheduleFlush() +} diff --git a/fileui_tabs.go b/fileui_tabs.go new file mode 100644 index 0000000..6587b32 --- /dev/null +++ b/fileui_tabs.go @@ -0,0 +1,295 @@ +package main + +import ( + "image" + + "gioui.org/font" + "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/widget" + "gioui.org/widget/material" + + "loov.dev/lensm/internal/codeview" + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/gui" +) + +type CodeTab struct { + Name string + Func disasm.Func + Code codeview.UI + Preview bool + Tab widget.Clickable + Close widget.Clickable +} + +func (ui *FileUI) activeTab() *CodeTab { + if !gui.InRange(ui.ActiveTab, len(ui.CodeTabs)) { + return nil + } + return ui.CodeTabs[ui.ActiveTab] +} + +func (ui *FileUI) activeCode() *codeview.UI { + tab := ui.activeTab() + if tab == nil { + return nil + } + return &tab.Code +} + +func (ui *FileUI) loadTabCode(tab *CodeTab, fn disasm.Func) { + tab.Name = fn.Name() + tab.Func = fn + tab.Code = codeview.UI{} + tab.Code.Code, ui.LoadError = fn.Load(ui.loadOptions()) + tab.Code.SelectedAsm = -1 + tab.Code.SelectedView = codeview.ViewGoAsm + tab.Code.ResetScroll() +} + +func (ui *FileUI) appendCodeTab(fn disasm.Func) *CodeTab { + if fn == nil { + return nil + } + tab := &CodeTab{} + ui.loadTabCode(tab, fn) + ui.CodeTabs = append(ui.CodeTabs, tab) + return tab +} + +// previewIndex returns the index of the single preview tab, or -1. +func (ui *FileUI) previewIndex() int { + for i, tab := range ui.CodeTabs { + if tab.Preview { + return i + } + } + return -1 +} + +// previewTab shows fn in a single reusable preview tab. Browsing the function +// list (keyboard up/down or clicking a name) replaces the preview in place +// instead of piling up permanent tabs. Clicking the tab keeps it open, see +// selectTab, which clears Preview. +func (ui *FileUI) previewTab(fn disasm.Func) *CodeTab { + if fn == nil { + return nil + } + name := fn.Name() + tab := ui.findCodeTab(name, fn) + if tab == nil { + if slot := ui.previewIndex(); slot >= 0 { + tab = ui.CodeTabs[slot] + ui.loadTabCode(tab, fn) + ui.ActiveTab = slot + } else { + tab = ui.appendCodeTab(fn) + if tab == nil { + return nil + } + ui.ActiveTab = len(ui.CodeTabs) - 1 + } + tab.Preview = true + } + ui.selectFuncByName(name) + ui.commentKey = "" + ui.recordNavigation(name) + ui.saveSessionState() + return tab +} + +// openTab opens fn in a tab and activates it. When next is true a newly +// created tab is inserted right after the active tab instead of appended +// at the end; an already-open tab is activated in place regardless. +func (ui *FileUI) openTab(fn disasm.Func, next bool) *CodeTab { + if fn == nil { + return nil + } + name := fn.Name() + tab := ui.findCodeTab(name, fn) + if tab == nil { + tab = ui.appendCodeTab(fn) + if tab == nil { + return nil + } + index := len(ui.CodeTabs) - 1 + if next && gui.InRange(ui.ActiveTab, index) { + at := ui.ActiveTab + 1 + if at < index { + copy(ui.CodeTabs[at+1:], ui.CodeTabs[at:index]) + ui.CodeTabs[at] = tab + index = at + } + } + ui.ActiveTab = index + } + tab.Preview = false + ui.selectFuncByName(name) + ui.commentKey = "" + ui.recordNavigation(name) + ui.saveSessionState() + return tab +} + +// findCodeTab returns the open tab for name, refreshing its Func and +// making it active, or nil if no tab is open for it. +func (ui *FileUI) findCodeTab(name string, fn disasm.Func) *CodeTab { + for i, tab := range ui.CodeTabs { + if tab.Name == name { + tab.Func = fn + ui.ActiveTab = i + return tab + } + } + return nil +} + +// keepActiveTab promotes the active preview tab to a permanent one, called +// when the user acts on the tab's content. +func (ui *FileUI) keepActiveTab() { + if tab := ui.activeTab(); tab != nil && tab.Preview { + tab.Preview = false + ui.saveSessionState() + } +} + +func (ui *FileUI) selectTab(index int) { + if !gui.InRange(index, len(ui.CodeTabs)) { + return + } + ui.ActiveTab = index + ui.CodeTabs[index].Preview = false + ui.commentKey = "" + ui.selectFuncByName(ui.CodeTabs[index].Name) + ui.recordNavigation(ui.CodeTabs[index].Name) + ui.saveSessionState() +} + +func (ui *FileUI) closeTab(index int) { + if !gui.InRange(index, len(ui.CodeTabs)) { + return + } + ui.CodeTabs = append(ui.CodeTabs[:index], ui.CodeTabs[index+1:]...) + switch { + case len(ui.CodeTabs) == 0: + ui.ActiveTab = -1 + ui.commentKey = "" + ui.Funcs.Selected = "" + ui.Funcs.SelectedItem = nil + ui.Funcs.List.Selected = -1 + case ui.ActiveTab > index: + ui.ActiveTab-- + case ui.ActiveTab == index && ui.ActiveTab >= len(ui.CodeTabs): + ui.ActiveTab = len(ui.CodeTabs) - 1 + } + if tab := ui.activeTab(); tab != nil { + ui.selectFuncByName(tab.Name) + ui.recordNavigation(tab.Name) + } else { + ui.commentKey = "" + } + ui.saveSessionState() +} + +func (ui *FileUI) layoutCodeTabs(gtx layout.Context, colors gui.UIColors) layout.Dimensions { + for i := 0; i < len(ui.CodeTabs); i++ { + tab := ui.CodeTabs[i] + for tab.Tab.Clicked(gtx) { + ui.selectTab(i) + } + closed := false + for tab.Close.Clicked(gtx) { + ui.closeTab(i) + closed = true + } + if closed { + i-- + } + } + + if len(ui.CodeTabs) == 0 { + return layout.Dimensions{} + } + + height := max(gtx.Metric.Dp(22), 20) + availableWidth := gtx.Constraints.Max.X + gtx.Constraints = layout.Exact(image.Pt(availableWidth, height)) + paint.FillShape(gtx.Ops, colors.SecondaryBackground, clip.Rect{Max: gtx.Constraints.Max}.Op()) + + tabWidth := gtx.Metric.Dp(220) + minTabWidth := gtx.Metric.Dp(120) + if len(ui.CodeTabs) <= 3 && availableWidth > 0 { + tabWidth = min(tabWidth, max(minTabWidth, availableWidth/len(ui.CodeTabs))) + } + if tabWidth < minTabWidth { + tabWidth = minTabWidth + } + + list := material.List(ui.Theme.Theme, &ui.Tabs) + list.AnchorStrategy = material.Overlay + return list.Layout(gtx, len(ui.CodeTabs), func(gtx layout.Context, index int) layout.Dimensions { + gtx.Constraints = layout.Exact(image.Pt(tabWidth, height)) + return ui.layoutCodeTab(gtx, colors, ui.CodeTabs[index], index == ui.ActiveTab) + }) +} + +func (ui *FileUI) layoutCodeTab(gtx layout.Context, colors gui.UIColors, tab *CodeTab, active bool) layout.Dimensions { + size := gtx.Constraints.Max + defer clip.Rect{Max: size}.Push(gtx.Ops).Pop() + + bg := colors.SecondaryBackground + if active { + bg = colors.Background + } else if tab.Tab.Hovered() { + bg = colors.Selection + } + paint.FillShape(gtx.Ops, bg, clip.Rect{Max: size}.Op()) + if active { + paint.FillShape(gtx.Ops, ui.Theme.ContrastBg, clip.Rect{Max: image.Pt(size.X, 2)}.Op()) + } + paint.FillShape(gtx.Ops, colors.Splitter, clip.Rect{ + Min: image.Pt(size.X-1, 0), + Max: size, + }.Op()) + + layout.Inset{Top: 1, Right: 4, Bottom: 1, Left: 8}.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + gtx.Constraints = layout.Exact(gtx.Constraints.Max) + return tab.Tab.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + size := gtx.Constraints.Max + defer clip.Rect{Max: size}.Push(gtx.Ops).Pop() + + label := ui.Theme.Label(tab.Name, 0.8) + label.MaxLines = 1 + if active { + label.Font.Weight = font.Black + } + if tab.Preview { + label.Font.Style = font.Italic + } + dims := layout.W.Layout(gtx, label.Layout) + return layout.Dimensions{Size: size, Baseline: dims.Baseline} + }) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + closeWidth := gtx.Metric.Dp(22) + gtx.Constraints = layout.Exact(image.Pt(closeWidth, gtx.Constraints.Max.Y)) + return tab.Close.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + size := gtx.Constraints.Max + if tab.Close.Hovered() { + paint.FillShape(gtx.Ops, colors.Selection, clip.Rect{Max: size}.Op()) + } + label := ui.Theme.Muted("x", 0.8) + label.MaxLines = 1 + dims := layout.Center.Layout(gtx, label.Layout) + return layout.Dimensions{Size: size, Baseline: dims.Baseline} + }) + }), + ) + }) + + return layout.Dimensions{Size: size} +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cc104ed --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module loov.dev/lensm + +go 1.26 + +require ( + gioui.org v0.10.1 + gioui.org/x v0.10.0 + github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 + golang.org/x/arch v0.28.0 + golang.org/x/exp/shiny v0.0.0-20260112195511-716be5621a96 +) + +require ( + gioui.org/shader v1.0.8 // indirect + git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 // indirect + github.com/go-text/typesetting v0.3.4 // indirect + github.com/godbus/dbus/v5 v5.0.6 // indirect + golang.org/x/exp v0.0.0-20260112195511-716be5621a96 // indirect + golang.org/x/image v0.35.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..c769d9d --- /dev/null +++ b/go.sum @@ -0,0 +1,41 @@ +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= +gioui.org v0.10.1 h1:Dvp6iDk9RKuZk19jxhOmb4p673CLVvb656LyMxQ+uO0= +gioui.org v0.10.1/go.mod h1:MZJZsdEPkTBzChdqeE8CiiQhreUQBj43qusDxQNDf7k= +gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= +gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= +gioui.org/x v0.10.0 h1:+oXnsUsyqQEldUR+4l+U25hHv6lU6TagZxXi9YDWEXE= +gioui.org/x v0.10.0/go.mod h1:ruS8Rj06tvag88dJmb8mrWbgmrcahiPZcBLd2ZKyQ6Q= +git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0 h1:bGG/g4ypjrCJoSvFrP5hafr9PPB5aw8SjcOWWila7ZI= +git.wow.st/gmp/jni v0.0.0-20210610011705-34026c7e22d0/go.mod h1:+axXBRUTIDlCeE73IKeD/os7LoEnTKdkp8/gQOFjqyo= +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/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU= +github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY= +github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos= +github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= +github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= +github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +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/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834 h1:ZF+QBjOI+tILZjBaFj3HgFonKXUcwgJ4djLb6i42S3Q= +github.com/tetratelabs/wabin v0.0.0-20230304001439-f6f874872834/go.mod h1:m9ymHTgNSEjuxvw8E7WWe4Pl4hZQHXONY8wE6dMLaRk= +golang.org/x/arch v0.28.0 h1:wVwVdqsTuUbJvhYVCspQYwZXHNYeLSoZnmHD+ggddpQ= +golang.org/x/arch v0.28.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96 h1:Z/6YuSHTLOHfNFdb8zVZomZr7cqNgTJvA8+Qz75D8gU= +golang.org/x/exp v0.0.0-20260112195511-716be5621a96/go.mod h1:nzimsREAkjBCIEFtHiYkrJyT+2uy9YZJB7H1k68CXZU= +golang.org/x/exp/shiny v0.0.0-20260112195511-716be5621a96 h1:wJ3cDLvYRAWzRt6f3e2VwVlziH3httfx2PGMa8hqqWo= +golang.org/x/exp/shiny v0.0.0-20260112195511-716be5621a96/go.mod h1:hq/Ge0xSczE7aHicXVhn3Kd0j3hOtWQR4KEgAwemgdk= +golang.org/x/image v0.35.0 h1:LKjiHdgMtO8z7Fh18nGY6KDcoEtVfsgLDPeLyguqb7I= +golang.org/x/image v0.35.0/go.mod h1:MwPLTVgvxSASsxdLzKrl8BRFuyqMyGhLwmC+TO1Sybk= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/icons.go b/icons.go new file mode 100644 index 0000000..a8af644 --- /dev/null +++ b/icons.go @@ -0,0 +1,12 @@ +package main + +import ( + "gioui.org/widget" + "golang.org/x/exp/shiny/materialdesign/icons" +) + +// SettingsIcon is used for application settings. +var SettingsIcon = func() *widget.Icon { + icon, _ := widget.NewIcon(icons.ActionSettings) + return icon +}() diff --git a/internal/README.md b/internal/README.md new file mode 100644 index 0000000..7593d90 --- /dev/null +++ b/internal/README.md @@ -0,0 +1 @@ +`go` folder contains code vendored from [Go project](https://go.googlesource.com/go/). \ No newline at end of file diff --git a/internal/asmcheck/main.go b/internal/asmcheck/main.go new file mode 100644 index 0000000..188d463 --- /dev/null +++ b/internal/asmcheck/main.go @@ -0,0 +1,92 @@ +// Command asmcheck reads `go tool objdump` output on stdin, extracts every Go +// (Plan 9) instruction mnemonic, and reports which ones asmhelp fails to +// resolve to real reference data. Throwaway analysis tool. +package main + +import ( + "bufio" + "fmt" + "os" + "sort" + "strings" + + "loov.dev/lensm/internal/asmhelp" +) + +func main() { + arch := "arm64" + if len(os.Args) > 1 { + arch = os.Args[1] + } + + count := map[string]int{} // mnemonic -> occurrences + sample := map[string]string{} // mnemonic -> one full instruction line + + sc := bufio.NewScanner(os.Stdin) + sc.Buffer(make([]byte, 1<<20), 1<<20) + for sc.Scan() { + line := sc.Text() + if strings.HasPrefix(strings.TrimSpace(line), "TEXT ") { + continue // function header pseudo-op, not an instruction + } + // objdump instruction lines are tab-separated (with empty padding + // fields): file:line, addr, hex, instr. The instruction is the last + // non-empty field. + var instr string + for _, f := range strings.Split(line, "\t") { + if f = strings.TrimSpace(f); f != "" { + instr = f + } + } + if !strings.HasPrefix(strings.TrimSpace(line), " ") && !strings.Contains(line, "0x") { + continue // function header line, not an instruction + } + toks := strings.Fields(instr) + if len(toks) == 0 { + continue + } + m := toks[0] + count[m]++ + if sample[m] == "" { + sample[m] = instr + } + } + + var unresolved, notPlausible []string + resolved := 0 + for m := range count { + help, ok := asmhelp.ForInstruction(arch, "", sample[m]) + switch { + case ok && help.Note == "": + resolved++ + case ok && help.Note != "": + unresolved = append(unresolved, m) + default: + notPlausible = append(notPlausible, m) + } + } + + byCountDesc := func(list []string) { + sort.Slice(list, func(i, j int) bool { + if count[list[i]] != count[list[j]] { + return count[list[i]] > count[list[j]] + } + return list[i] < list[j] + }) + } + byCountDesc(unresolved) + byCountDesc(notPlausible) + + fmt.Printf("unique mnemonics: %d resolved: %d unresolved: %d not-plausible: %d\n\n", + len(count), resolved, len(unresolved), len(notPlausible)) + fmt.Println("UNRESOLVED (plausible mnemonics with no reference data), by frequency:") + for _, m := range unresolved { + fmt.Printf(" %6d %-12s %s\n", count[m], m, sample[m]) + } + if len(notPlausible) > 0 { + fmt.Println("\nNOT-PLAUSIBLE (skipped: directives, undecodable, etc.):") + for _, m := range notPlausible { + fmt.Printf(" %6d %s\n", count[m], m) + } + } +} diff --git a/internal/asmhelp/asmhelp.go b/internal/asmhelp/asmhelp.go new file mode 100644 index 0000000..6139def --- /dev/null +++ b/internal/asmhelp/asmhelp.go @@ -0,0 +1,964 @@ +package asmhelp + +import ( + "regexp" + "slices" + "strings" + + "loov.dev/lensm/internal/asmref" +) + +type Help struct { + Mnemonic string + Description string + Explanation string + // Ports lists execution-port usage (uops.info notation) for x86 mnemonics, + // sourced from the generated reference. Empty for other architectures. + Ports []string + // Note flags that the tooltip has no real reference data for this mnemonic; + // the Description is a generic placeholder. Empty when reference data exists. + Note string +} + +type asmInstructionRule struct { + Prefixes []string + Description string + Explain func([]string) string + // ExplainArch is used instead of Explain when the operand meaning + // depends on the architecture. + ExplainArch func(arch string, operands []string) string +} + +// asmInstructionRules is intentionally data-driven: add a prefix and, when +// useful, a small operand rewrite to extend the built-in reference. +var asmInstructionRules = []asmInstructionRule{ + {Prefixes: []string{"MOVZX", "MOVSX", "MOVSXD"}, Description: "Move a value while extending it to a wider size.", Explain: explainMove}, + {Prefixes: []string{"MOV"}, Description: "Move or copy data between registers and memory.", Explain: explainMove}, + {Prefixes: []string{"LEA"}, Description: "Load an effective address without reading memory.", Explain: explainLEA}, + {Prefixes: []string{"ADD", "ADC"}, Description: "Add values; ADC also includes the carry flag.", Explain: explainBinary("+")}, + {Prefixes: []string{"SUB", "SBB"}, Description: "Subtract values; SBB also includes the borrow flag.", Explain: explainBinary("-")}, + {Prefixes: []string{"MUL", "IMUL"}, Description: "Multiply values (IMUL is signed multiplication).", Explain: explainBinary("*")}, + {Prefixes: []string{"MADD"}, Description: "Multiply two values and add a third.", Explain: explainMADD}, + {Prefixes: []string{"MSUB"}, Description: "Multiply two values and subtract the product from a third.", Explain: explainMSUB}, + {Prefixes: []string{"DIV", "IDIV", "SDIV", "UDIV"}, Description: "Divide values (signed or unsigned according to the mnemonic).", Explain: explainBinary("/")}, + {Prefixes: []string{"CQO", "CDQ", "CWD", "CBW"}, Description: "Sign-extend the accumulator for a wider signed operation."}, + {Prefixes: []string{"AND"}, Description: "Compute a bitwise AND.", Explain: explainBinary("&")}, + {Prefixes: []string{"OR", "ORR"}, Description: "Compute a bitwise OR.", Explain: explainBinary("|")}, + {Prefixes: []string{"BIC"}, Description: "Clear selected bits of a value.", Explain: explainBinary("&^")}, + {Prefixes: []string{"XOR", "EOR"}, Description: "Compute a bitwise exclusive OR.", Explain: explainBinary("^")}, + {Prefixes: []string{"SHL", "SAL", "LSL"}, Description: "Shift bits left.", Explain: explainBinary("<<")}, + {Prefixes: []string{"SHR", "LSR"}, Description: "Shift bits right, filling with zeroes.", Explain: explainBinary(">>")}, + {Prefixes: []string{"SAR", "ASR"}, Description: "Arithmetic right shift, preserving the sign.", Explain: explainBinary(">>")}, + {Prefixes: []string{"INC"}, Description: "Increment a value by one.", Explain: explainUnaryDelta("+", "1")}, + {Prefixes: []string{"DEC"}, Description: "Decrement a value by one.", Explain: explainUnaryDelta("-", "1")}, + {Prefixes: []string{"NEG"}, Description: "Negate a signed value.", Explain: explainUnaryPrefix("-")}, + {Prefixes: []string{"NOT", "MVN"}, Description: "Invert every bit in a value.", Explain: explainUnaryPrefix("^")}, + {Prefixes: []string{"CMP"}, Description: "Compare values and update condition flags.", ExplainArch: explainCompare}, + {Prefixes: []string{"CMOV"}, Description: "Conditionally move a value when the selected flags match.", Explain: explainMove}, + {Prefixes: []string{"SET"}, Description: "Set a byte to 0 or 1 according to condition flags."}, + {Prefixes: []string{"CSEL", "CSET"}, Description: "Select or set a value according to condition flags."}, + {Prefixes: []string{"TEST", "TST"}, Description: "Test bits and update condition flags without storing a result.", Explain: explainTest}, + {Prefixes: []string{"FMADD"}, Description: "Fused floating-point multiply-add with one rounding step.", Explain: explainFMADD}, + {Prefixes: []string{"FADD"}, Description: "Add floating-point values.", Explain: explainBinary("+")}, + {Prefixes: []string{"FSUB"}, Description: "Subtract floating-point values.", Explain: explainBinary("-")}, + {Prefixes: []string{"FMUL"}, Description: "Multiply floating-point values.", Explain: explainBinary("*")}, + {Prefixes: []string{"FMOV"}, Description: "Move a floating-point value.", Explain: explainMove}, + {Prefixes: []string{"LDR", "LDUR", "LOAD"}, Description: "Load a value from memory into a register.", Explain: explainLoad}, + {Prefixes: []string{"STR", "STUR", "STORE"}, Description: "Store a register value in memory.", Explain: explainStore}, + {Prefixes: []string{"ADR", "ADRP"}, Description: "Form a PC-relative address."}, + {Prefixes: []string{"LDP"}, Description: "Load a pair of registers from memory."}, + {Prefixes: []string{"STP"}, Description: "Store a pair of registers to memory."}, + {Prefixes: []string{"XCHG", "SWAP"}, Description: "Exchange two values."}, + {Prefixes: []string{"BSWAP", "REV"}, Description: "Reverse the byte order of a value."}, + {Prefixes: []string{"BSF", "BSR"}, Description: "Scan for the position of the first or last set bit."}, + {Prefixes: []string{"CLZ", "CTZ"}, Description: "Count leading or trailing zero bits."}, + {Prefixes: []string{"POPCNT"}, Description: "Count the one bits in a value."}, + {Prefixes: []string{"PUSH"}, Description: "Push a value onto the stack."}, + {Prefixes: []string{"POP"}, Description: "Pop the top stack value."}, + {Prefixes: []string{"CALL", "BL", "BLR", "JAL", "JALR"}, Description: "Call a function and save a return address."}, + {Prefixes: []string{"LOCK"}, Description: "Make the following read-modify-write instruction atomic."}, + {Prefixes: []string{"FS", "GS"}, Description: "Segment override prefix for the following memory access."}, + {Prefixes: []string{"SYSCALL", "SVC"}, Description: "Enter the operating system or supervisor."}, + {Prefixes: []string{"INT"}, Description: "Raise a software interrupt."}, + {Prefixes: []string{"ICEBP"}, Description: "Raise a debug exception (undocumented INT1)."}, + {Prefixes: []string{"RET", "LRET"}, Description: "Return to the caller."}, + {Prefixes: []string{"JMP", "B", "BR"}, Description: "Jump to another instruction."}, + {Prefixes: []string{"JE", "JZ", "BEQ"}, Description: "Jump when values are equal (zero flag set)."}, + {Prefixes: []string{"JNE", "JNZ", "BNE"}, Description: "Jump when values are not equal."}, + {Prefixes: []string{"JG", "JGE", "JL", "JLE", "BGT", "BGE", "BLT", "BLE"}, Description: "Conditional jump after a signed comparison."}, + {Prefixes: []string{"JA", "JAE", "JB", "JBE", "BHI", "BHS", "BLO", "BLS", "BCS", "BCC"}, Description: "Conditional jump after an unsigned comparison."}, + {Prefixes: []string{"BMI", "BPL", "BVS", "BVC"}, Description: "Conditional jump testing the sign or overflow flag."}, + {Prefixes: []string{"CBZ"}, Description: "Jump when a register is zero."}, + {Prefixes: []string{"CBNZ"}, Description: "Jump when a register is not zero."}, + {Prefixes: []string{"TBZ"}, Description: "Jump when a selected bit is zero."}, + {Prefixes: []string{"TBNZ"}, Description: "Jump when a selected bit is not zero."}, + {Prefixes: []string{"NOP", "NOOP"}, Description: "Do nothing for one instruction slot."}, + {Prefixes: []string{"HLT", "WFI"}, Description: "Stop or wait for an external event."}, + {Prefixes: []string{"PAUSE", "YIELD"}, Description: "Hint that the processor is in a spin-wait loop."}, + {Prefixes: []string{"MFENCE", "LFENCE", "SFENCE", "DMB", "DSB", "ISB"}, Description: "Order memory or instruction accesses across this barrier."}, +} + +// NativeAssemblyInstructionHelp returns reference text and a syntax-correct +// effect for the native (GNU) spelling of an instruction. GNU x86/AT&T uses +// source-first operands while ARM-family GNU syntax uses destination-first. +func ForNative(arch, canonical, text string) (Help, bool) { + mnemonic, operands := splitAssemblyInstruction(text) + lookup := canonicalNativeMnemonic(mnemonic) + // Arch is irrelevant here: only the description is kept, the + // explanation is replaced with a native-syntax rewrite below. + help, ok := knownAssemblyInstructionHelp("", lookup, nil) + if !ok { + return Help{}, false + } + help.Mnemonic = mnemonic + help.Explanation = explainNativeInstruction(lookup, operands) + if help.Explanation == "" { + help.Explanation = explainNativeEffect(lookup, operands) + } + if ref, ok := referenceEntry(arch, canonical, mnemonic); ok && isX86(arch) { + help.Ports = referencePorts(ref) + } + return help, true +} + +func canonicalNativeMnemonic(mnemonic string) string { + switch { + case mnemonic == "ADDS": + return "ADD" + case mnemonic == "SUBS": + return "SUB" + case mnemonic == "ANDS": + return "AND" + case strings.HasPrefix(mnemonic, "LDR"): + return "LDR" + case strings.HasPrefix(mnemonic, "LDUR"): + return "LDUR" + case strings.HasPrefix(mnemonic, "STR"): + return "STR" + case strings.HasPrefix(mnemonic, "STUR"): + return "STUR" + case strings.HasPrefix(mnemonic, "CMOV"): + return "CMOV" + case strings.HasPrefix(mnemonic, "SET"): + return "SET" + case strings.HasPrefix(mnemonic, "MOVZ"), strings.HasPrefix(mnemonic, "MOVSX"), strings.HasPrefix(mnemonic, "MOVSXD"): + return "MOVZX" + case mnemonic == "CQTO": + return "CQO" + case strings.HasPrefix(mnemonic, "B."): + condition := strings.TrimPrefix(mnemonic, "B.") + switch condition { + case "EQ": + return "BEQ" + case "NE": + return "BNE" + case "GT": + return "BGT" + case "GE": + return "BGE" + case "LT": + return "BLT" + case "LE": + return "BLE" + case "HI": + return "BHI" + case "HS", "CS": + return "BHS" + case "LO", "CC": + return "BLO" + case "LS": + return "BLS" + case "MI": + return "BMI" + case "PL": + return "BPL" + case "VS": + return "BVS" + case "VC": + return "BVC" + case "AL", "NV": + // Both encode "always" on modern cores. + return "B" + } + } + for _, base := range []string{"PUSH", "POP", "CALL", "JMP", "RET", "XCHG", "POPCNT", "BSWAP", "INC", "DEC", "NEG", "NOT"} { + if mnemonic == base+"B" || mnemonic == base+"W" || mnemonic == base+"L" || mnemonic == base+"Q" { + return base + } + } + return mnemonic +} + +func explainNativeEffect(mnemonic string, operands []string) string { + operand := func(index int) string { + if index >= len(operands) { + return "target" + } + return strings.TrimSpace(strings.TrimPrefix(operands[index], "#")) + } + switch { + case mnemonic == "PUSH": + return "stack := push(stack, " + operand(0) + ")" + case mnemonic == "POP": + return operand(0) + " := pop(stack)" + case mnemonic == "CALL" || mnemonic == "BL" || mnemonic == "BLR" || mnemonic == "JAL" || mnemonic == "JALR": + return "call " + operand(0) + " and save the return address" + case mnemonic == "RET": + return "PC := saved return address" + case mnemonic == "JMP" || mnemonic == "B" || mnemonic == "BR": + return "PC := " + operand(0) + case mnemonic == "JE" || mnemonic == "JZ" || mnemonic == "BEQ": + return "if the compared values are equal (Z == 1), PC := " + operand(0) + case mnemonic == "JNE" || mnemonic == "JNZ" || mnemonic == "BNE": + return "if the compared values are not equal (Z == 0), PC := " + operand(0) + case mnemonic == "BGT": + return "if signed greater than (Z == 0 and N == V), PC := " + operand(0) + case mnemonic == "BGE": + return "if signed greater than or equal (N == V), PC := " + operand(0) + case mnemonic == "BLT": + return "if signed less than (N != V), PC := " + operand(0) + case mnemonic == "BLE": + return "if signed less than or equal (Z == 1 or N != V), PC := " + operand(0) + case mnemonic == "JG": + return "if signed greater than (ZF == 0 and SF == OF), PC := " + operand(0) + case mnemonic == "JGE": + return "if signed greater than or equal (SF == OF), PC := " + operand(0) + case mnemonic == "JL": + return "if signed less than (SF != OF), PC := " + operand(0) + case mnemonic == "JLE": + return "if signed less than or equal (ZF == 1 or SF != OF), PC := " + operand(0) + case mnemonic == "BHI": + return "if unsigned higher than (C == 1 and Z == 0), PC := " + operand(0) + case mnemonic == "BHS": + return "if unsigned higher than or equal (C == 1), PC := " + operand(0) + case mnemonic == "BLO": + return "if unsigned lower than (C == 0), PC := " + operand(0) + case mnemonic == "BLS": + return "if unsigned lower than or equal (C == 0 or Z == 1), PC := " + operand(0) + case mnemonic == "BMI": + return "if negative (N == 1), PC := " + operand(0) + case mnemonic == "BPL": + return "if positive or zero (N == 0), PC := " + operand(0) + case mnemonic == "BVS": + return "if overflow (V == 1), PC := " + operand(0) + case mnemonic == "BVC": + return "if no overflow (V == 0), PC := " + operand(0) + case mnemonic == "JA": + return "if unsigned above (CF == 0 and ZF == 0), PC := " + operand(0) + case mnemonic == "JAE": + return "if unsigned above or equal (CF == 0), PC := " + operand(0) + case mnemonic == "JB": + return "if unsigned below (CF == 1), PC := " + operand(0) + case mnemonic == "JBE": + return "if unsigned below or equal (CF == 1 or ZF == 1), PC := " + operand(0) + case mnemonic == "CBZ": + return "if " + operand(0) + " == 0, PC := " + operand(1) + case mnemonic == "CBNZ": + return "if " + operand(0) + " != 0, PC := " + operand(1) + case mnemonic == "TBZ": + return "if bit " + operand(1) + " of " + operand(0) + " == 0, PC := " + operand(2) + case mnemonic == "TBNZ": + return "if bit " + operand(1) + " of " + operand(0) + " != 0, PC := " + operand(2) + case mnemonic == "INC": + return operand(0) + " := " + operand(0) + " + 1" + case mnemonic == "DEC": + return operand(0) + " := " + operand(0) + " - 1" + case mnemonic == "NEG": + return operand(0) + " := -" + operand(0) + case mnemonic == "NOT": + return operand(0) + " := ^" + operand(0) + case mnemonic == "SET": + return operand(0) + " := condition ? 1 : 0" + case mnemonic == "NOP": + return "state is unchanged" + case mnemonic == "SYSCALL" || mnemonic == "SVC" || mnemonic == "INT": + return "transfer control to the operating system" + case mnemonic == "MFENCE" || mnemonic == "LFENCE" || mnemonic == "SFENCE" || mnemonic == "DMB" || mnemonic == "DSB" || mnemonic == "ISB": + return "wait for the ordered accesses to complete" + case mnemonic == "PAUSE" || mnemonic == "YIELD": + return "temporarily yield processor execution resources" + case mnemonic == "HLT" || mnemonic == "WFI": + return "wait until an interrupt or event occurs" + case mnemonic == "CQO" || mnemonic == "CDQ" || mnemonic == "CWD" || mnemonic == "CBW": + return "extend the accumulator's sign into the high half" + case mnemonic == "XCHG" || mnemonic == "SWAP": + return operand(0) + ", " + operand(1) + " := " + operand(1) + ", " + operand(0) + } + return "" +} + +func explainNativeInstruction(mnemonic string, operands []string) string { + if len(operands) == 0 { + return "" + } + // Registers prefixed with % and immediates prefixed with $ identify GNU + // x86/AT&T syntax, whose source-first operand order agrees with the + // source-first Plan 9 rewrites (hence the empty arch: the "amd64" CMP + // rewrite is natural-order, which would be wrong for AT&T). + if strings.Contains(strings.Join(operands, " "), "%") || strings.HasPrefix(strings.TrimSpace(operands[0]), "$") { + if help, ok := ForInstruction("", "", mnemonic+" "+strings.Join(operands, ", ")); ok { + if help.Explanation != "" { + return help.Explanation + } + } + return explainNativeX86Effect(mnemonic, operands) + } + + value := formatNativeValue + if len(operands) >= 2 { + destination := value(operands[0]) + switch { + case mnemonic == "MOVZX", strings.HasPrefix(mnemonic, "MOV"): + return destination + " := " + value(operands[1]) + case mnemonic == "LDR" || mnemonic == "LDUR" || mnemonic == "LOAD": + return explainNativeLoad(destination, operands[1:]) + case mnemonic == "STR" || mnemonic == "STUR" || mnemonic == "STORE": + return explainNativeStore(value(operands[0]), operands[1:]) + case mnemonic == "LDP" && len(operands) >= 3: + return explainNativePairLoad(value(operands[0]), value(operands[1]), operands[2:]) + case mnemonic == "STP" && len(operands) >= 3: + return explainNativePairStore(value(operands[0]), value(operands[1]), operands[2:]) + case mnemonic == "LEA", mnemonic == "ADR", mnemonic == "ADRP": + return destination + " := address(" + value(operands[1]) + ")" + case mnemonic == "ADD" || mnemonic == "ADC": + return explainNativeDestinationFirst(destination, operands[1:], "+", value) + case mnemonic == "SUB" || mnemonic == "SBB": + return explainNativeDestinationFirst(destination, operands[1:], "-", value) + case mnemonic == "MUL" || mnemonic == "IMUL" || mnemonic == "FMUL": + return explainNativeDestinationFirst(destination, operands[1:], "*", value) + case mnemonic == "MADD" && len(operands) >= 4: + return destination + " := " + value(operands[1]) + " * " + value(operands[2]) + " + " + value(operands[3]) + case mnemonic == "MSUB" && len(operands) >= 4: + return destination + " := " + value(operands[3]) + " - " + value(operands[1]) + " * " + value(operands[2]) + case mnemonic == "NEG": + // arm64 two-operand form: neg x0, x1 is x0 := -x1. + return destination + " := -" + value(operands[1]) + case mnemonic == "NOT" || mnemonic == "MVN": + return destination + " := ^" + value(operands[1]) + case mnemonic == "DIV" || mnemonic == "IDIV" || mnemonic == "SDIV" || mnemonic == "UDIV": + return explainNativeDestinationFirst(destination, operands[1:], "/", value) + case mnemonic == "AND": + return explainNativeDestinationFirst(destination, operands[1:], "&", value) + case mnemonic == "BIC": + return explainNativeDestinationFirst(destination, operands[1:], "&^", value) + case mnemonic == "OR" || mnemonic == "ORR": + return explainNativeDestinationFirst(destination, operands[1:], "|", value) + case mnemonic == "XOR" || mnemonic == "EOR": + return explainNativeDestinationFirst(destination, operands[1:], "^", value) + case mnemonic == "SHL" || mnemonic == "SAL" || mnemonic == "LSL": + return explainNativeDestinationFirst(destination, operands[1:], "<<", value) + case mnemonic == "SHR" || mnemonic == "LSR" || mnemonic == "SAR" || mnemonic == "ASR": + return explainNativeDestinationFirst(destination, operands[1:], ">>", value) + case mnemonic == "FADD": + return explainNativeDestinationFirst(destination, operands[1:], "+", value) + case mnemonic == "FSUB": + return explainNativeDestinationFirst(destination, operands[1:], "-", value) + case mnemonic == "FMADD" && len(operands) >= 4: + return destination + " := " + value(operands[1]) + " * " + value(operands[2]) + " + " + value(operands[3]) + case mnemonic == "CMP": + return "flags := compare(" + value(operands[0]) + ", " + value(operands[1]) + ")" + case mnemonic == "TEST" || mnemonic == "TST": + return "flags := " + value(operands[0]) + " & " + value(operands[1]) + case mnemonic == "CSEL" && len(operands) >= 4: + return destination + " := condition(" + value(operands[3]) + ") ? " + value(operands[1]) + " : " + value(operands[2]) + case mnemonic == "CSET": + return destination + " := condition(" + value(operands[1]) + ") ? 1 : 0" + case mnemonic == "POPCNT": + return destination + " := countOneBits(" + value(operands[1]) + ")" + case mnemonic == "BSWAP" || mnemonic == "REV": + return destination + " := reverseBytes(" + value(operands[1]) + ")" + case mnemonic == "BSF" || mnemonic == "BSR" || mnemonic == "CLZ" || mnemonic == "CTZ": + return destination + " := significantBitPosition(" + value(operands[1]) + ")" + } + } + return "" +} + +func explainNativeX86Effect(mnemonic string, operands []string) string { + operand := func(index int) string { + if index >= len(operands) { + return "target" + } + return formatNativeValue(operands[index]) + } + destination := operand(len(operands) - 1) + switch mnemonic { + case "POPCNT": + return destination + " := countOneBits(" + operand(0) + ")" + case "BSWAP": + return destination + " := reverseBytes(" + destination + ")" + case "XCHG": + return operand(0) + ", " + operand(1) + " := " + operand(1) + ", " + operand(0) + case "SET": + return destination + " := condition ? 1 : 0" + case "CMOV": + return "if condition, " + destination + " := " + operand(0) + } + return explainNativeEffect(mnemonic, operands) +} + +func formatNativeValue(operand string) string { + operand = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(operand), "#")) + if strings.HasPrefix(operand, "[") { + return formatNativeMemory(operand) + } + return operand +} + +func formatNativeMemory(operand string) string { + _, address, _ := parseNativeMemory(operand) + return "memory[" + address + "]" +} + +func explainNativeLoad(destination string, addressOperands []string) string { + base, address, preIndex := parseNativeMemory(addressOperands[0]) + if preIndex { + return base + " := " + address + "; " + destination + " := memory[" + base + "]" + } + effect := destination + " := memory[" + address + "]" + if len(addressOperands) > 1 { + effect += "; " + base + " := " + addNativeOffset(base, addressOperands[1]) + } + return effect +} + +func explainNativeStore(source string, addressOperands []string) string { + base, address, preIndex := parseNativeMemory(addressOperands[0]) + if preIndex { + return base + " := " + address + "; memory[" + base + "] := " + source + } + effect := "memory[" + address + "] := " + source + if len(addressOperands) > 1 { + effect += "; " + base + " := " + addNativeOffset(base, addressOperands[1]) + } + return effect +} + +func explainNativePairLoad(first, second string, addressOperands []string) string { + base, address, preIndex := parseNativeMemory(addressOperands[0]) + load := first + ", " + second + " := pair(memory[" + address + "])" + if preIndex { + return base + " := " + address + "; " + first + ", " + second + " := pair(memory[" + base + "])" + } + if len(addressOperands) > 1 { + load += "; " + base + " := " + addNativeOffset(base, addressOperands[1]) + } + return load +} + +func explainNativePairStore(first, second string, addressOperands []string) string { + base, address, preIndex := parseNativeMemory(addressOperands[0]) + store := "memory[" + address + "] := pair(" + first + ", " + second + ")" + if preIndex { + return base + " := " + address + "; memory[" + base + "] := pair(" + first + ", " + second + ")" + } + if len(addressOperands) > 1 { + store += "; " + base + " := " + addNativeOffset(base, addressOperands[1]) + } + return store +} + +func parseNativeMemory(operand string) (base, address string, preIndex bool) { + operand = strings.TrimSpace(operand) + preIndex = strings.HasSuffix(operand, "!") + inside := strings.TrimSpace(strings.TrimSuffix(strings.TrimSuffix(strings.TrimPrefix(operand, "["), "!"), "]")) + parts := splitAssemblyOperands(inside) + base = strings.TrimSpace(parts[0]) + address = base + if len(parts) > 1 { + address = addNativeOffset(base, strings.Join(parts[1:], ", ")) + } + return base, address, preIndex +} + +func addNativeOffset(base, offset string) string { + offset = strings.TrimSpace(strings.ReplaceAll(offset, "#", "")) + if after, ok := strings.CutPrefix(offset, "-"); ok { + return base + " - " + after + } + return base + " + " + offset +} + +func explainNativeDestinationFirst(destination string, sources []string, operator string, value func(string) string) string { + if len(sources) == 1 { + return destination + " := " + destination + " " + operator + " " + value(sources[0]) + } + return destination + " := " + value(sources[0]) + " " + operator + " " + value(sources[1]) +} + +// ForInstruction returns help for a Go-assembly instruction. canonical is the +// decoder's mnemonic (e.g. "LD1" for the Go spelling "VLD1"); pass "" when it is +// unavailable and the mnemonic is recovered from text instead. +func ForInstruction(arch, canonical, text string) (Help, bool) { + display, operands := splitAssemblyInstruction(text) + if display == "" { + return Help{}, false + } + // Strip the Go assembler's .P/.W index marker before matching, so post- and + // pre-index forms (MOVBU.P, FLDPQ.P) resolve like their base mnemonic. + mnemonic := display + if i := strings.IndexByte(mnemonic, '.'); i > 0 { + mnemonic = mnemonic[:i] + } + ref, hasRef := referenceEntry(arch, canonical, mnemonic) + + help, ok := knownAssemblyInstructionHelp(arch, mnemonic, operands) + switch { + case ok: + // Curated rules own the bespoke Explanation semantics; keep them. + case hasRef: + // For mnemonics the rules don't cover, fall back to the generated + // reference so the tooltip shows real ARM/x86 text. + help, ok = Help{Description: referenceBrief(ref)}, true + case plausibleMnemonic(mnemonic): + help = Help{ + Description: "Execute the " + mnemonic + " instruction.", + Note: noReferenceNote, + } + default: + return Help{}, false + } + // Ports are x86-only in the reference; the table merges ARM and x86 under + // one mnemonic key, so gate by arch to avoid showing x86 ports for arm64. + if hasRef && isX86(arch) { + help.Ports = referencePorts(ref) + } + help.Mnemonic = display + return help, true +} + +func isX86(arch string) bool { + return arch == "386" || arch == "amd64" +} + +// referenceArch is the microarchitecture whose port usage is summarised in the +// tooltip. The table stores every measured microarchitecture; the hover shows +// one recent, well-covered core. +const referenceArch = "ADL-P" + +// referencePorts returns the distinct port usages measured for the reference +// microarchitecture across the mnemonic's operand forms. +func referencePorts(ref asmref.Entry) []string { + var out []string + for _, p := range ref.PerfFor(referenceArch) { + if p.Ports != "" && !slices.Contains(out, p.Ports) { + out = append(out, p.Ports) + } + } + return out +} + +// noReferenceNote is shown on tooltips that have no real reference data — the +// Description is only a generic placeholder. +const noReferenceNote = "No reference information available for this instruction." + +// referenceEntry looks up the generated reference for an instruction. It tries +// the canonical decoder mnemonic first (authoritative), then falls back to +// spellings recovered from the displayed text: the table is keyed by base +// mnemonic (ADD, LD1, CRC32) while the disassembly shows ADDQ, CRC32Q (x86 size +// suffixes) or VLD1.P, VMOV (arm64 SIMD V-prefix and .P post-index marker). +func referenceEntry(arch, canonical, textMnemonic string) (asmref.Entry, bool) { + for _, m := range []string{canonical, textMnemonic} { + if m == "" { + continue + } + for _, candidate := range mnemonicCandidates(arch, m) { + if e, ok := asmref.Lookup(candidate); ok { + return e, true + } + } + } + return asmref.Entry{}, false +} + +// mnemonicCandidates returns the base mnemonics to try, most specific first. +func mnemonicCandidates(arch, mnemonic string) []string { + candidates := []string{mnemonic} + base := mnemonic + // Go assembler dot-suffixes mark writeback/post-index (VLD1.P -> VLD1). + if i := strings.IndexByte(base, '.'); i > 0 { + base = base[:i] + candidates = append(candidates, base) + } + if isX86(arch) { + // Go disambiguates some SSE ops with a _XMM tag (MOVSD_XMM -> MOVSD). + if i := strings.IndexByte(base, '_'); i > 0 { + base = base[:i] + candidates = append(candidates, base) + } + if s := trimGoAsmSuffix(base); s != base { + candidates = append(candidates, s) + } + return candidates + } + // arm64: expand the Go spelling into candidate ARM base mnemonics. Only + // table hits are used, so over-eager candidates that match nothing are + // harmless; rule-covered mnemonics (e.g. branches) never reach here. + forms := []string{base} + // Go spells SIMD ops with a leading V (VLD1 -> LD1, VMOV -> MOV). + if strings.HasPrefix(base, "V") && len(base) > 1 { + forms = append(forms, base[1:]) + } + for _, f := range append([]string(nil), forms...) { + // Trailing type/width suffixes: W (32-bit), B/H/S/D/Q (element size), + // and two-letter combos on converts (FCVTZSDW -> FCVTZS). + for n := 1; n <= 2; n++ { + if len(f) > n && isArmSizeSuffix(f[len(f)-n:]) { + forms = append(forms, f[:len(f)-n]) + } + } + // "2" upper-half variants (PMULL2, UMLAL2) are documented under the base. + if strings.HasSuffix(f, "2") && len(f) > 1 { + forms = append(forms, f[:len(f)-1]) + } + // SIMD&FP register-pair load/store: FLDP/FSTP -> LDP/STP. + if strings.HasPrefix(f, "FLDP") { + forms = append(forms, "LDP") + } else if strings.HasPrefix(f, "FSTP") { + forms = append(forms, "STP") + } + } + return append(candidates, forms...) +} + +func isArmSizeSuffix(s string) bool { + for _, r := range s { + switch r { + case 'B', 'H', 'W', 'S', 'D', 'Q': + default: + return false + } + } + return true +} + +// trimGoAsmSuffix removes a single trailing Go-assembler operand-size suffix. +// Longer suffixes (BU/HU/WU) are tried before single letters. Only used after +// an exact lookup misses, so stripping a genuine trailing letter is harmless. +func trimGoAsmSuffix(mnemonic string) string { + for _, suffix := range []string{"BU", "HU", "WU", "B", "W", "L", "Q", "D", "H", "S"} { + if len(mnemonic) > len(suffix) && strings.HasSuffix(mnemonic, suffix) { + return mnemonic[:len(mnemonic)-len(suffix)] + } + } + return mnemonic +} + +// referenceBrief returns a short human description for an entry, preferring the +// brief title over the full first paragraph. +func referenceBrief(ref asmref.Entry) string { + if ref.Brief != "" { + return ref.Brief + } + return ref.Description +} + +// plausibleMnemonic reports whether a token looks like an instruction +// mnemonic. Undecodable bytes render as "?", which must not get an +// authoritative-sounding "Execute the ? instruction." fallback. +func plausibleMnemonic(mnemonic string) bool { + if r := mnemonic[0]; r < 'A' || r > 'Z' { + return false + } + for _, r := range mnemonic { + switch { + case 'A' <= r && r <= 'Z', '0' <= r && r <= '9', r == '.', r == '_': + default: + return false + } + } + return true +} + +func knownAssemblyInstructionHelp(arch, mnemonic string, operands []string) (Help, bool) { + // Resolve exact mnemonics first. Otherwise BLS can be mistaken for BL with + // an S size suffix, and similar prefix collisions produce wrong semantics. + for _, rule := range asmInstructionRules { + if slices.Contains(rule.Prefixes, mnemonic) { + return assemblyHelpFromRule(arch, mnemonic, operands, rule), true + } + } + for _, rule := range asmInstructionRules { + for _, prefix := range rule.Prefixes { + if mnemonicMatches(mnemonic, prefix) { + return assemblyHelpFromRule(arch, mnemonic, operands, rule), true + } + } + } + return Help{}, false +} + +func assemblyHelpFromRule(arch, mnemonic string, operands []string, rule asmInstructionRule) Help { + help := Help{Mnemonic: mnemonic, Description: rule.Description} + switch { + case rule.ExplainArch != nil: + help.Explanation = rule.ExplainArch(arch, operands) + case rule.Explain != nil: + help.Explanation = rule.Explain(operands) + } + return help +} + +func mnemonicMatches(mnemonic, prefix string) bool { + if mnemonic == prefix { + return true + } + if !strings.HasPrefix(mnemonic, prefix) { + return false + } + suffix := strings.TrimPrefix(mnemonic, prefix) + // CMOV/SET take an x86 condition-code suffix (CMOVNE, SETA) rather than a + // size suffix. + if conditionFamilies[prefix] { + return x86ConditionCodes[suffix] + } + return nativeSizeSuffixes(prefix)[suffix] +} + +// conditionFamilies are mnemonics whose suffix is a condition code, not a size. +var conditionFamilies = map[string]bool{"CMOV": true, "SET": true} + +var x86ConditionCodes = map[string]bool{ + "E": true, "NE": true, "Z": true, "NZ": true, "A": true, "AE": true, + "B": true, "BE": true, "G": true, "GE": true, "L": true, "LE": true, + "O": true, "NO": true, "S": true, "NS": true, "P": true, "PE": true, + "NP": true, "PO": true, "C": true, "NC": true, +} + +// Suffix sets are package-level: nativeSizeSuffixes runs in the per-frame +// hover lookup and must not allocate. +var ( + x86IntegerSuffixes = map[string]bool{"B": true, "W": true, "L": true, "Q": true} + floatOrVectorSuffixes = map[string]bool{"S": true, "D": true} + // arm64 Go assembly sizes moves with D/H/W/B plus zero-extending + // BU/HU/WU: MOVD, MOVWU, ... The x86 sizes stay valid alongside. + movSuffixes = map[string]bool{ + "B": true, "W": true, "L": true, "Q": true, + "D": true, "H": true, "BU": true, "HU": true, "WU": true, + } + // arm64 32-bit register variants: SDIVW, MADDW, LSLW, ... + arm64WordSuffixes = map[string]bool{"W": true} +) + +func nativeSizeSuffixes(mnemonic string) map[string]bool { + switch mnemonic { + case "MOV": + return movSuffixes + case "FMOV": + return floatOrVectorSuffixes + case "LEA", "ADD", "ADC", "SUB", "SBB", "MUL", "IMUL", "DIV", "IDIV", + "AND", "OR", "XOR", "SHL", "SAL", "SHR", "SAR", "INC", "DEC", "NEG", "NOT", + "CMP", "TEST", "PUSH", "POP", "CALL", "RET", "XCHG", "BSWAP", "POPCNT": + return x86IntegerSuffixes + case "SDIV", "UDIV", "MADD", "MSUB", "ORR", "EOR", "LSL", "LSR", "ASR", "BIC", "MVN": + return arm64WordSuffixes + case "FADD", "FSUB", "FMUL", "FMADD": + return floatOrVectorSuffixes + default: + return nil + } +} + +func splitAssemblyInstruction(text string) (string, []string) { + text = strings.TrimSpace(text) + if text == "" { + return "", nil + } + if index := strings.Index(text, "\t"); index >= 0 { + text = text[:index] + } + fields := strings.Fields(text) + if len(fields) == 0 { + return "", nil + } + mnemonic := strings.ToUpper(strings.TrimSpace(fields[0])) + rest := strings.TrimSpace(strings.TrimPrefix(text, fields[0])) + if rest == "" { + return mnemonic, nil + } + parts := splitAssemblyOperands(rest) + operands := make([]string, 0, len(parts)) + for _, part := range parts { + operands = append(operands, strings.TrimSpace(part)) + } + return mnemonic, operands +} + +func splitAssemblyOperands(text string) []string { + var parts []string + start, depth := 0, 0 + for index, char := range text { + switch char { + case '(', '[': + depth++ + case ')', ']': + if depth > 0 { + depth-- + } + case ',': + if depth == 0 { + parts = append(parts, text[start:index]) + start = index + 1 + } + } + } + return append(parts, text[start:]) +} + +func explainMove(operands []string) string { + if len(operands) < 2 { + return "" + } + return formatDestination(operands[len(operands)-1]) + " := " + formatValue(operands[0]) +} + +func explainLEA(operands []string) string { + if len(operands) < 2 { + return "" + } + return formatDestination(operands[len(operands)-1]) + " := " + formatAddress(operands[0]) +} + +func explainBinary(operator string) func([]string) string { + return func(operands []string) string { + if len(operands) < 2 { + return "" + } + destination := formatDestination(operands[len(operands)-1]) + if len(operands) == 2 { + return destination + " := " + destination + " " + operator + " " + formatValue(operands[0]) + } + // Three-operand Go assembly computes dst := second op first: + // SUB R1, R5, R3 is R3 = R5 - R1, LSL $3, R5, R3 is R3 = R5 << 3. + return destination + " := " + formatValue(operands[len(operands)-2]) + " " + operator + " " + formatValue(operands[0]) + } +} + +func explainUnaryDelta(operator, amount string) func([]string) string { + return func(operands []string) string { + if len(operands) == 0 { + return "" + } + destination := formatDestination(operands[len(operands)-1]) + return destination + " := " + destination + " " + operator + " " + amount + } +} + +func explainUnaryPrefix(operator string) func([]string) string { + return func(operands []string) string { + if len(operands) == 0 { + return "" + } + destination := formatDestination(operands[len(operands)-1]) + return destination + " := " + operator + destination + } +} + +func explainCompare(arch string, operands []string) string { + if len(operands) < 2 { + return "" + } + // Go assembly for x86 deliberately keeps CMP operands in natural + // left-to-right order (CMPQ AX, BX sets flags from AX-BX; see + // go.dev/issue/60920), while arm64 and the other ports use + // source-first order (CMP R1, R4 sets flags from R4-R1). + if arch == "386" || arch == "amd64" { + return "flags := compare(" + formatValue(operands[0]) + ", " + formatValue(operands[1]) + ")" + } + return "flags := compare(" + formatValue(operands[1]) + ", " + formatValue(operands[0]) + ")" +} + +func explainTest(operands []string) string { + if len(operands) < 2 { + return "" + } + return "flags := " + formatValue(operands[1]) + " & " + formatValue(operands[0]) +} + +func explainFMADD(operands []string) string { + if len(operands) < 4 { + return "" + } + destination := formatDestination(operands[3]) + return destination + " := " + formatValue(operands[0]) + " * " + formatValue(operands[2]) + " + " + formatValue(operands[1]) +} + +// Go arm64 assembly orders MADD/MSUB as "Rm, Ra, Rn, Rd": +// MADD R2, R3, R1, R0 computes R0 = R1*R2 + R3. +func explainMADD(operands []string) string { + if len(operands) < 4 { + return "" + } + destination := formatDestination(operands[3]) + return destination + " := " + formatValue(operands[2]) + " * " + formatValue(operands[0]) + " + " + formatValue(operands[1]) +} + +func explainMSUB(operands []string) string { + if len(operands) < 4 { + return "" + } + destination := formatDestination(operands[3]) + return destination + " := " + formatValue(operands[1]) + " - " + formatValue(operands[2]) + " * " + formatValue(operands[0]) +} + +func explainLoad(operands []string) string { + if len(operands) < 2 { + return "" + } + return formatDestination(operands[len(operands)-1]) + " := " + formatValue(operands[0]) +} + +func explainStore(operands []string) string { + if len(operands) < 2 { + return "" + } + return "memory[" + formatAddress(operands[len(operands)-1]) + "] := " + formatValue(operands[0]) +} + +var plan9Address = regexp.MustCompile(`^([^()]*)\(([^)]+)\)(?:\(([^)]+)\))?$`) + +func formatValue(operand string) string { + operand = strings.TrimSpace(operand) + if after, ok := strings.CutPrefix(operand, "$"); ok { + return after + } + if plan9Address.MatchString(operand) { + return "memory[" + formatAddress(operand) + "]" + } + return operand +} + +func formatDestination(operand string) string { + operand = strings.TrimSpace(operand) + if plan9Address.MatchString(operand) { + return "memory[" + formatAddress(operand) + "]" + } + return operand +} + +func formatAddress(operand string) string { + operand = strings.TrimSpace(strings.TrimPrefix(operand, "$")) + match := plan9Address.FindStringSubmatch(operand) + if len(match) == 0 { + return operand + } + displacement := strings.TrimSpace(match[1]) + base := strings.TrimSpace(match[2]) + index := strings.TrimSpace(match[3]) + + var terms []string + if base != "" && base != "SB" { + terms = append(terms, base) + } + if index != "" { + index = strings.ReplaceAll(index, "*", " * ") + terms = append(terms, index) + } + if displacement != "" && displacement != "0" { + terms = append(terms, displacement) + } + if len(terms) == 0 { + return displacement + } + return strings.Join(terms, " + ") +} diff --git a/internal/asmhelp/asmhelp_test.go b/internal/asmhelp/asmhelp_test.go new file mode 100644 index 0000000..8bebefa --- /dev/null +++ b/internal/asmhelp/asmhelp_test.go @@ -0,0 +1,388 @@ +package asmhelp + +import ( + "strings" + "testing" +) + +func TestAssemblyInstructionExplanations(t *testing.T) { + tests := []struct { + arch string + instruction string + want string + }{ + {"arm64", "MOV (R2), R1", "R1 := memory[R2]"}, + {"amd64", "ADDQ $1, R1", "R1 := R1 + 1"}, + {"amd64", "LEAQ 0(R1)(R2*4), R3", "R3 := R1 + R2 * 4"}, + {"arm64", "FMADDS F0, F2, F1, F2", "F2 := F0 * F1 + F2"}, + // x86 Go assembly keeps CMP operands in natural order: flags are + // computed from AX - 0x10 (go.dev/issue/60920). + {"amd64", "CMPQ AX, $0x10", "flags := compare(AX, 0x10)"}, + // arm64 Go assembly is source-first: CMP R1, R4 is R4 - R1. + {"arm64", "CMP R1, R4", "flags := compare(R4, R1)"}, + // Three-operand arm64 forms compute dst := second op first. + {"arm64", "SUB R1, R5, R3", "R3 := R5 - R1"}, + {"arm64", "LSL $3, R5, R3", "R3 := R5 << 3"}, + // arm64 sized moves and 32-bit variants must keep matching their + // base rules. + {"arm64", "MOVD (R1), R0", "R0 := memory[R1]"}, + {"arm64", "MOVWU (R1), R0", "R0 := memory[R1]"}, + {"arm64", "SDIVW R1, R2, R0", "R0 := R2 / R1"}, + {"arm64", "FMOVD F1, F0", "F0 := F1"}, + // MADD R2, R3, R1, R0 computes R0 = R1*R2 + R3. + {"arm64", "MADD R2, R3, R1, R0", "R0 := R1 * R2 + R3"}, + {"arm64", "MSUB R2, R3, R1, R0", "R0 := R3 - R1 * R2"}, + } + for _, test := range tests { + help, ok := ForInstruction(test.arch, "", test.instruction) + if !ok { + t.Fatalf("no help for %q", test.instruction) + } + if help.Explanation != test.want { + t.Errorf("explanation for %q (%s) = %q, want %q", test.instruction, test.arch, help.Explanation, test.want) + } + } +} + +func TestAssemblyInstructionReference(t *testing.T) { + help, ok := ForInstruction("amd64", "", "JNE 12(PC)") + if !ok || help.Description == "" { + t.Fatalf("JNE help = %#v, %v", help, ok) + } + if help.Explanation != "" { + t.Fatalf("unexpected JNE explanation: %q", help.Explanation) + } +} + +func TestUnknownGoAssemblyInstructionHasFallbackReference(t *testing.T) { + // A plausible mnemonic that is in neither the curated rules nor the + // generated reference falls back to the generic line, flagged with a note. + help, ok := ForInstruction("amd64", "", "ZZUNKNOWNOP $49, Y1, Y2, Y3") + if !ok { + t.Fatal("no fallback help for Go assembly instruction") + } + if help.Mnemonic != "ZZUNKNOWNOP" || help.Description != "Execute the ZZUNKNOWNOP instruction." { + t.Fatalf("fallback help = %#v", help) + } + if help.Note == "" { + t.Error("generic fallback should carry a missing-reference note") + } +} + +// TestGoAssemblerSpellingsResolve covers the mnemonic spellings the Go +// assembler emits that differ from the ARM/x86 reference names, found by +// disassembling the lensm binary. Each must resolve to real help (no +// missing-reference note). +func TestGoAssemblerSpellingsResolve(t *testing.T) { + cases := []struct{ arch, mnemonic string }{ + // arm64 width/type suffixes, V-prefix, .P index, F-pair, "2" variant. + {"arm64", "LDPW"}, {"arm64", "STPW"}, {"arm64", "CBZW"}, {"arm64", "TSTW"}, + {"arm64", "REV16W"}, {"arm64", "MOVKW"}, {"arm64", "LDARW"}, + {"arm64", "FCMPS"}, {"arm64", "FDIVD"}, {"arm64", "FMOVQ"}, + {"arm64", "SCVTFWD"}, {"arm64", "FCVTZSDW"}, {"arm64", "UCVTFD"}, + {"arm64", "FLDPQ"}, {"arm64", "FSTPS"}, {"arm64", "FLDPQ.P"}, + {"arm64", "VLD1.P"}, {"arm64", "VMOV"}, {"arm64", "VPMULL2"}, + {"arm64", "MOVBU.P"}, {"arm64", "BCC"}, {"arm64", "NOOP"}, + // amd64 condition families, prefixes, SSE tag, obscure ops. + {"amd64", "CMOVNE"}, {"amd64", "CMOVAE"}, {"amd64", "SETA"}, {"amd64", "SETNE"}, + {"amd64", "LOCK"}, {"amd64", "MOVSD_XMM"}, {"amd64", "ICEBP"}, + {"amd64", "FS"}, {"amd64", "LRET"}, + } + for _, tc := range cases { + help, ok := ForInstruction(tc.arch, "", tc.mnemonic) + if !ok || help.Note != "" || help.Description == "" { + t.Errorf("%s %s did not resolve: ok=%v %#v", tc.arch, tc.mnemonic, ok, help) + } + } +} + +func TestCanonicalMnemonicIsAuthoritative(t *testing.T) { + // The decoder's canonical op name resolves the reference directly, even when + // the displayed mnemonic is unrecognisable and no heuristic would map it. + help, ok := ForInstruction("arm64", "FMOV", "ZZZ F0, F1") + if !ok || help.Note != "" || help.Description == "" { + t.Fatalf("canonical FMOV did not resolve: ok=%v %#v", ok, help) + } + if help.Mnemonic != "ZZZ" { + t.Errorf("display mnemonic = %q, want ZZZ (from text)", help.Mnemonic) + } +} + +func TestReferenceResolvesArm64GoSpellings(t *testing.T) { + // The Go arm64 assembler spells SIMD ops with a V prefix and a .P + // post-index marker; the reference is keyed by the ARM base name (LD1, MOV). + for _, instruction := range []string{ + "VLD1 (R0), [V0.B16]", + "VLD1.P 16(R0), [V0.B16]", + "VMOV V1.B16, V2.B16", + } { + help, ok := ForInstruction("arm64", "", instruction) + if !ok { + t.Fatalf("no help for %q", instruction) + } + if help.Description == "" || help.Note != "" { + t.Errorf("%q should resolve to real reference text, got %#v", instruction, help) + } + } +} + +func TestGeneratedReferenceReplacesGenericFallback(t *testing.T) { + // ABS and CRC32 are absent from the curated rules, so they used to get the + // generic "Execute the X instruction." line. The generated asmref table now + // supplies real reference text, while the bespoke Explanation stays empty + // (no rule fabricates semantics for them). Asserted structurally so the test + // holds against whatever prose the current ISA release ships. + for _, tc := range []struct{ arch, instruction string }{ + {"arm64", "ABS V0.8B, V1.8B"}, + {"amd64", "CRC32 AX, BL"}, + } { + mnemonic, _ := splitAssemblyInstruction(tc.instruction) + help, ok := ForInstruction(tc.arch, "", tc.instruction) + if !ok { + t.Fatalf("no help for %q", tc.instruction) + } + if help.Description == "" || help.Description == "Execute the "+mnemonic+" instruction." { + t.Errorf("%q got generic/empty description %q", tc.instruction, help.Description) + } + if help.Explanation != "" { + t.Errorf("unexpected explanation for %q: %q", tc.instruction, help.Explanation) + } + } +} + +func TestReferenceLookupToleratesPlan9Suffixes(t *testing.T) { + // lensm shows Plan 9 spellings with size suffixes (CRC32Q), but the table + // is keyed by the base mnemonic (CRC32). The fallback must still resolve it. + help, ok := ForInstruction("amd64", "", "CRC32Q AX, BX") + if !ok { + t.Fatal("no help for CRC32Q") + } + if !strings.Contains(strings.ToUpper(help.Description), "CRC32") { + t.Errorf("CRC32Q description = %q, want it to mention CRC32", help.Description) + } + if len(help.Ports) == 0 { + t.Errorf("CRC32Q should carry ports from the reference") + } +} + +func TestPortsAttachToRuleCoveredX86ButNotArm(t *testing.T) { + // ADDQ is covered by the curated rules; ports still come from the reference. + x86, ok := ForInstruction("amd64", "", "ADDQ $1, AX") + if !ok { + t.Fatal("no help for ADDQ") + } + if len(x86.Ports) == 0 { + t.Errorf("amd64 ADDQ should carry ports, got %#v", x86) + } + // The same merged ADD entry must not leak its x86 ports onto arm64. + arm, ok := ForInstruction("arm64", "", "ADD R1, R2, R3") + if !ok { + t.Fatal("no help for arm64 ADD") + } + if len(arm.Ports) != 0 { + t.Errorf("arm64 ADD must not carry x86 ports, got %#v", arm.Ports) + } +} + +func TestUndecodableInstructionHasNoFallback(t *testing.T) { + // Undecodable bytes render as "?" in the Go column. + if help, ok := ForInstruction("amd64", "", "?"); ok { + t.Fatalf("unexpected fallback for undecodable instruction: %#v", help) + } + if help, ok := ForInstruction("amd64", "", "// pseudo"); ok { + t.Fatalf("unexpected fallback for non-mnemonic token: %#v", help) + } +} + +func TestUnknownNativeAssemblyInstructionHasNoGoFallback(t *testing.T) { + if help, ok := ForNative("", "", "unknownop %rax"); ok { + t.Fatalf("unexpected native fallback: %#v", help) + } +} + +func TestNativeAssemblyInstructionHelpUsesNativeRewrite(t *testing.T) { + help, ok := ForNative("", "", "addq $1, %rax") + if !ok { + t.Fatal("no native help for ADDQ") + } + if help.Explanation != "%rax := %rax + 1" { + t.Fatalf("native explanation = %q", help.Explanation) + } +} + +func TestNativeARMAssemblyInstructionExplanation(t *testing.T) { + help, ok := ForNative("", "", "add x0, x1, #8") + if !ok { + t.Fatal("no native help for ARM ADD") + } + if help.Explanation != "x0 := x1 + 8" { + t.Fatalf("native ARM explanation = %q", help.Explanation) + } +} + +func TestNativeARMStoreInstructionExplanation(t *testing.T) { + help, ok := ForNative("", "", "str x0, [sp, #16]") + if !ok { + t.Fatal("no native help for ARM STR") + } + if help.Explanation != "memory[sp + 16] := x0" { + t.Fatalf("native ARM STR explanation = %q", help.Explanation) + } +} + +func TestNativeARMIndexedMemoryExplanations(t *testing.T) { + tests := map[string]string{ + "str x30, [sp, #-112]!": "sp := sp - 112; memory[sp] := x30", + "ldr x0, [sp], #16": "x0 := memory[sp]; sp := sp + 16", + "stp x29, x30, [sp, #-16]!": "sp := sp - 16; memory[sp] := pair(x29, x30)", + } + for instruction, want := range tests { + help, ok := ForNative("", "", instruction) + if !ok || help.Explanation != want { + t.Errorf("%q explanation = %q, want %q", instruction, help.Explanation, want) + } + } +} + +func TestNativeDirectJumpHasHelp(t *testing.T) { + // x86 GNU syntax spells direct jumps jmpq. + help, ok := ForNative("", "", "jmpq .+0x100") + if !ok { + t.Fatal("no native help for jmpq") + } + if help.Explanation != "PC := .+0x100" { + t.Fatalf("jmpq explanation = %q", help.Explanation) + } +} + +func TestNativeARMTwoOperandNeg(t *testing.T) { + help, ok := ForNative("", "", "neg x0, x1") + if !ok { + t.Fatal("no native help for neg") + } + if help.Explanation != "x0 := -x1" { + t.Fatalf("neg explanation = %q", help.Explanation) + } + help, ok = ForNative("", "", "mvn x0, x1") + if !ok { + t.Fatal("no native help for mvn") + } + if help.Explanation != "x0 := ^x1" { + t.Fatalf("mvn explanation = %q", help.Explanation) + } +} + +func TestNativeARMUnsignedConditionalBranchIsNotCall(t *testing.T) { + help, ok := ForNative("", "", "b.ls .+0x1bc") + if !ok { + t.Fatal("no native help for B.LS") + } + if help.Description != "Conditional jump after an unsigned comparison." { + t.Fatalf("B.LS description = %q", help.Description) + } + if help.Explanation != "if unsigned lower than or equal (C == 0 or Z == 1), PC := .+0x1bc" { + t.Fatalf("B.LS explanation = %q", help.Explanation) + } +} + +func TestAssemblyHelpRuleTableHasNoMnemonicCollisions(t *testing.T) { + type owner struct { + description string + prefix string + } + exact := make(map[string]owner) + for _, rule := range asmInstructionRules { + for _, prefix := range rule.Prefixes { + if previous, exists := exact[prefix]; exists { + t.Errorf("duplicate mnemonic %s in %s and %s", prefix, previous.prefix, prefix) + } + exact[prefix] = owner{description: rule.Description, prefix: prefix} + } + } + + for _, rule := range asmInstructionRules { + for _, prefix := range rule.Prefixes { + for suffix := range nativeSizeSuffixes(prefix) { + mnemonic := prefix + suffix + if exactOwner, isExactMnemonic := exact[mnemonic]; isExactMnemonic { + if help, ok := ForInstruction("", "", mnemonic); !ok || help.Description != exactOwner.description { + t.Errorf("exact %s was captured as a suffix of %s", mnemonic, prefix) + } + continue + } + help, ok := ForInstruction("", "", mnemonic) + if !ok || help.Description != rule.Description { + t.Errorf("%s resolved to %#v; want rule for %s", mnemonic, help, prefix) + } + } + } + } +} + +func TestAllNativeARMConditionBranchesResolveAsBranches(t *testing.T) { + conditions := map[string]string{ + "eq": "if the compared values are equal (Z == 1), PC := .+4", + "ne": "if the compared values are not equal (Z == 0), PC := .+4", + "gt": "if signed greater than (Z == 0 and N == V), PC := .+4", + "ge": "if signed greater than or equal (N == V), PC := .+4", + "lt": "if signed less than (N != V), PC := .+4", + "le": "if signed less than or equal (Z == 1 or N != V), PC := .+4", + "hi": "if unsigned higher than (C == 1 and Z == 0), PC := .+4", + "hs": "if unsigned higher than or equal (C == 1), PC := .+4", + "cs": "if unsigned higher than or equal (C == 1), PC := .+4", + "lo": "if unsigned lower than (C == 0), PC := .+4", + "cc": "if unsigned lower than (C == 0), PC := .+4", + "ls": "if unsigned lower than or equal (C == 0 or Z == 1), PC := .+4", + } + for condition, want := range conditions { + instruction := "b." + condition + " .+4" + help, ok := ForNative("", "", instruction) + if !ok { + t.Errorf("no help for %s", instruction) + continue + } + if strings.Contains(help.Description, "Call a function") { + t.Errorf("%s incorrectly resolved as call: %#v", instruction, help) + } + if help.Explanation != want { + t.Errorf("%s effect = %q, want %q", instruction, help.Explanation, want) + } + } +} + +func TestAssemblyInstructionReferenceCoverage(t *testing.T) { + for _, instruction := range []string{ + "movzbl (%rax), %eax", "cmovne %rax, %rbx", "sete %al", + "cqto", "syscall", "mfence", "adrp x0, 0x1000", "stp x0, x1, [sp]", + } { + if help, ok := ForNative("", "", instruction); !ok || help.Description == "" || help.Explanation == "" { + t.Errorf("no native help for %q", instruction) + } + } +} + +func TestNativeInstructionFamiliesHaveConcreteExplanations(t *testing.T) { + instructions := []string{ + "movq %rax, %rbx", "movzbl (%rax), %ebx", "leaq 8(%rax), %rbx", + "addq $1, %rax", "sub x0, x1, x2", "madd x0, x1, x2, x3", + "msub x0, x1, x2, x3", "udiv x0, x1, x2", "and x0, x1, x2", + "orr x0, x1, x2", "eor x0, x1, x2", "lsl x0, x1, #3", + "cmp x0, x1", "tst x0, x1", "fadd d0, d1, d2", + "fsub d0, d1, d2", "fmul d0, d1, d2", "fmadd d0, d1, d2, d3", + "ldrb w0, [x1, #4]", "strh w0, [sp, #8]", "ldp x0, x1, [sp]", + "stp x0, x1, [sp, #-16]!", "adrp x0, 0x1000", "csel x0, x1, x2, eq", + "cset x0, ne", "rev x0, x1", "clz x0, x1", "popcnt %rax, %rbx", + "pushq %rax", "popq %rax", "callq 0x1000", "ret", "jmp 0x1000", + "b.eq 0x1000", "cbz x0, 0x1000", "tbnz x0, #2, 0x1000", + "nop", "syscall", "dmb ish", "yield", "xchgq %rax, %rbx", + } + for _, instruction := range instructions { + help, ok := ForNative("", "", instruction) + if !ok || help.Explanation == "" { + t.Errorf("native %q help has no concrete explanation: %#v", instruction, help) + } + if strings.HasPrefix(help.Explanation, "execute ") { + t.Errorf("native %q uses generic explanation %q", instruction, help.Explanation) + } + } +} diff --git a/internal/asmref/README.md b/internal/asmref/README.md new file mode 100644 index 0000000..dd92877 --- /dev/null +++ b/internal/asmref/README.md @@ -0,0 +1,54 @@ +# asmref + +A generated, flattened instruction reference for hover tooltips. `table.json.gz` +(gzip-compressed JSON) is embedded at build time and looked up by mnemonic via +`asmref.Lookup`. + +The table holds the factual "API" — brief title, description, syntax forms and +per-operand meanings — plus x86 per-microarchitecture performance data (uops, +ports, latency, throughput). Bit encodings and execution pseudocode are dropped. +The Go-pseudocode effects shown in tooltips come from `internal/asmhelp`, not +from here; `asmref` only fills in reference text `asmhelp` doesn't already cover. + +The full table (all mnemonics, all measured microarchitectures) is ~15MB of JSON +that gzips to ~0.6MB, hence the compressed embed. + +## Regenerating + +``` +go generate ./internal/asmref +``` + +With no arguments the generator reads the small checked-in fixtures under +`gen/testdata`, so a bare run stays reproducible. To build the full table, +download the sources (`data/download.sh`) and point it at them: + +``` +go run ./internal/asmref/gen \ + -arm data/arm64/ISA_A64_xml_A_profile-2025-12 \ + -x86 data/x86/instructions.xml \ + -out internal/asmref/table.json.gz +``` + +The generator also prints (to stderr) any mnemonics that `golang.org/x/arch`'s +decoders know but the table is missing — a coverage hint, not an error. + +## Sources + +- **ARM AArch64** — the official "Exploration tools / ISA XML" release from + developer.arm.com (the `ISA_A64_xml_*` bundle, one XML file per instruction). + Files starting with `sysreg_` (system registers) and `AArch64-` (shared + pseudocode) are skipped. +- **x86 / AMD64** — `instructions.xml` from https://uops.info (XED-derived, + ~140MB). It is a benchmark dataset, not a manual: there are no description or + syntax elements, so the `summary` attribute becomes the brief and the `string` + attribute (e.g. `ADD (R32, M32)`) becomes a syntax form. Per-operand + descriptions are not emitted — they restate the token already in the form. + For every operand form and every measured microarchitecture, the first + `` yields uops, ports (uops.info notation, e.g. `1*p0156`), + throughput and worst-case latency; `` estimate nodes are skipped in + favour of the real measurements. + +`data/download.sh` fetches both dumps (URLs/version pinned to match +`golang.org/x/arch/arm64/instgen/xmlspec`). They are large and gitignored; +record the release version you used so regeneration stays reproducible. diff --git a/internal/asmref/asmref.go b/internal/asmref/asmref.go new file mode 100644 index 0000000..d6d130b --- /dev/null +++ b/internal/asmref/asmref.go @@ -0,0 +1,94 @@ +// Package asmref holds a generated, flattened instruction reference used for +// hover tooltips. The table is produced by ./gen from CPU ISA XML (ARM's +// official AArch64 release and the uops.info XED-derived dump) and embedded as +// gzip-compressed JSON — see README.md for how to regenerate. +// +// asmref is deliberately just reference text (brief, description, syntax, +// operand meanings) plus x86 performance data. The bespoke Go-pseudocode +// effects live in internal/asmhelp; nothing here overwrites them. +package asmref + +import ( + "bytes" + "compress/gzip" + _ "embed" + "encoding/json" + "io" + "strings" + "sync" +) + +// Entry is the flattened reference for a single mnemonic. It matches the JSON +// schema written by the generator; both gen and runtime share this type. +type Entry struct { + Brief string `json:"brief,omitempty"` + Description string `json:"description,omitempty"` + Syntax []string `json:"syntax,omitempty"` + Operands map[string]string `json:"operands,omitempty"` + // Variants holds x86 per-operand-form performance data across all measured + // microarchitectures. Empty for ARM. + Variants []Variant `json:"variants,omitempty"` +} + +// Variant is one x86 operand form (e.g. "ADD (R32, R32)") and its measured +// performance on each microarchitecture. +type Variant struct { + Form string `json:"form"` + Perf []ArchPerf `json:"perf,omitempty"` +} + +// ArchPerf is the uops.info measurement for one microarchitecture. +type ArchPerf struct { + Arch string `json:"arch"` + Uops int `json:"uops,omitempty"` + Ports string `json:"ports,omitempty"` // uops.info notation, e.g. "1*p0156" + Latency int `json:"lat,omitempty"` // worst-case cycles across operand pairs + TP float64 `json:"tp,omitempty"` // throughput (cycles per instruction) +} + +// PerfFor returns the per-variant measurements for one microarchitecture. +func (e Entry) PerfFor(arch string) []ArchPerf { + var out []ArchPerf + for _, v := range e.Variants { + for _, p := range v.Perf { + if p.Arch == arch { + out = append(out, p) + } + } + } + return out +} + +// Regenerating needs the real ISA dumps under data/ (run data/download.sh +// first). The ARM directory name tracks the pinned release; bump both here and +// in data/download.sh when updating. +//go:generate go run ./gen -arm ../../data/arm64/ISA_A64_xml_A_profile-2025-12 -x86 ../../data/x86/instructions.xml -out table.json.gz + +//go:embed table.json.gz +var tableGz []byte + +var ( + tableOnce sync.Once + table map[string]Entry +) + +func load() { + // A malformed embed should surface as "no data" rather than panic in the + // UI hover path; the generator is what guarantees validity. + if r, err := gzip.NewReader(bytes.NewReader(tableGz)); err == nil { + if data, err := io.ReadAll(r); err == nil { + _ = json.Unmarshal(data, &table) + } + } + if table == nil { + table = map[string]Entry{} + } +} + +// Lookup returns the reference entry for a mnemonic (case-insensitive). The +// table is loaded once on first use. +func Lookup(mnemonic string) (Entry, bool) { + tableOnce.Do(load) + e, ok := table[strings.ToUpper(strings.TrimSpace(mnemonic))] + return e, ok +} diff --git a/internal/asmref/asmref_test.go b/internal/asmref/asmref_test.go new file mode 100644 index 0000000..9de304f --- /dev/null +++ b/internal/asmref/asmref_test.go @@ -0,0 +1,17 @@ +package asmref + +import "testing" + +func TestLookup(t *testing.T) { + // The embedded table is generated from gen/testdata; ADD is always present. + e, ok := Lookup("add") + if !ok { + t.Fatal("ADD not found (is table.json generated?)") + } + if e.Brief == "" || len(e.Syntax) == 0 { + t.Fatalf("ADD entry looks empty: %#v", e) + } + if _, ok := Lookup("NOTAREALINSTRUCTION"); ok { + t.Fatal("unexpected hit for bogus mnemonic") + } +} diff --git a/internal/asmref/gen/arm.go b/internal/asmref/gen/arm.go new file mode 100644 index 0000000..d18efc3 --- /dev/null +++ b/internal/asmref/gen/arm.go @@ -0,0 +1,226 @@ +package main + +import ( + "encoding/xml" + "fmt" + "io" + "os" + "path/filepath" + "slices" + "strings" +) + +// armCollected holds the syntaxes and operand meanings gathered for one +// mnemonic within a single instruction file. +type armCollected struct { + Syntax []string + Operands map[string]string +} + +// armFile is the reference extracted from one AArch64 instruction XML file. +type armFile struct { + Brief string // short title, from + Description string // first authored paragraph + Mnemonics map[string]*armCollected +} + +// ParseARMDir walks an ARM AArch64 ISA XML directory and feeds every +// instruction file into the builder. Files whose basename starts with +// "sysreg_" (system registers) or "AArch64-" (shared pseudocode, not +// instructions) are skipped per the ARM release layout. +func ParseARMDir(b *Builder, dir string) error { + return filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + return nil + } + name := d.Name() + if !strings.HasSuffix(name, ".xml") { + return nil + } + if strings.HasPrefix(name, "sysreg_") || strings.HasPrefix(name, "AArch64-") { + return nil + } + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + parsed, err := parseARMFile(f) + if err != nil { + return fmt.Errorf("%s: %w", name, err) + } + for mnemonic, c := range parsed.Mnemonics { + b.Add(mnemonic, parsed.Brief, parsed.Description, c.Syntax, nil, c.Operands) + } + return nil + }) +} + +// parseARMFile extracts the factual reference from one instruction file. It is +// a token walk rather than struct unmarshaling because is mixed +// content (literal text interleaved with operand links) and because +// (pseudocode) and (bit encodings) must be skipped +// wholesale. +func parseARMFile(r io.Reader) (armFile, error) { + dec := xml.NewDecoder(r) + out := armFile{Mnemonics: map[string]*armCollected{}} + + var ( + curMnemonic string + aliasMnemonic string // set for alias files (e.g. UBFIZ aliasing UBFM) + title string // instructionsection title attr, a fallback brief + + inBrief bool // inside + inBriefPara bool // capturing the first of + haveBrief bool + briefBuf strings.Builder + + inAuthored bool // inside + inDescPara bool // capturing the first of + haveDesc bool + descBuf strings.Builder + + inAsm bool // inside an + synBuf strings.Builder + inA bool // inside an operand link within an asmtemplate + aHover string + aTextBuf strings.Builder + ) + + // An alias file documents the alias (UBFIZ), not its underlying base + // (UBFM) — the mnemonic docvar holds the base, so the alias_mnemonic wins. + mnemonic := func() string { + if aliasMnemonic != "" { + return aliasMnemonic + } + return curMnemonic + } + + collected := func() *armCollected { + key := mnemonic() + c := out.Mnemonics[key] + if c == nil { + c = &armCollected{Operands: map[string]string{}} + out.Mnemonics[key] = c + } + return c + } + + for { + tok, err := dec.Token() + if err == io.EOF { + break + } + if err != nil { + return armFile{}, err + } + switch t := tok.(type) { + case xml.StartElement: + switch t.Name.Local { + case "instructionsection": + title = attr(t, "title") + case "ps_section", "regdiagram": + if err := dec.Skip(); err != nil { + return armFile{}, err + } + case "docvar": + switch attr(t, "key") { + case "mnemonic": + curMnemonic = attr(t, "value") + case "alias_mnemonic": + aliasMnemonic = attr(t, "value") + } + case "brief": + inBrief = true + case "authored": + inAuthored = true + case "para": + if inBrief && !haveBrief { + inBriefPara = true + briefBuf.Reset() + } else if inAuthored && !haveDesc { + inDescPara = true + descBuf.Reset() + } + case "asmtemplate": + inAsm = true + synBuf.Reset() + case "a": + if inAsm { + inA = true + aHover = attr(t, "hover") + aTextBuf.Reset() + } + } + case xml.CharData: + if inBriefPara { + briefBuf.Write(t) + } + if inDescPara { + descBuf.Write(t) + } + if inAsm { + synBuf.Write(t) + if inA { + aTextBuf.Write(t) + } + } + case xml.EndElement: + switch t.Name.Local { + case "brief": + inBrief = false + case "authored": + inAuthored = false + case "para": + if inBriefPara { + out.Brief = briefBuf.String() + haveBrief = true + inBriefPara = false + } else if inDescPara { + out.Description = descBuf.String() + haveDesc = true + inDescPara = false + } + case "a": + if inA { + name := strings.TrimSpace(aTextBuf.String()) + if name != "" && aHover != "" && mnemonic() != "" { + collected().Operands[name] = aHover + } + inA = false + } + case "asmtemplate": + inAsm = false + if mnemonic() != "" { + if syn := normalizeSpace(synBuf.String()); syn != "" { + c := collected() + if !slices.Contains(c.Syntax, syn) { + c.Syntax = append(c.Syntax, syn) + } + } + } + } + } + } + // Fall back to the title attr (minus its " -- A64" profile suffix) when a + // file has no paragraph. + if out.Brief == "" { + if i := strings.Index(title, " -- "); i >= 0 { + title = title[:i] + } + out.Brief = title + } + return out, nil +} + +func attr(e xml.StartElement, key string) string { + for _, a := range e.Attr { + if a.Name.Local == key { + return a.Value + } + } + return "" +} diff --git a/internal/asmref/gen/arm_test.go b/internal/asmref/gen/arm_test.go new file mode 100644 index 0000000..8123b9b --- /dev/null +++ b/internal/asmref/gen/arm_test.go @@ -0,0 +1,66 @@ +package main + +import ( + "strings" + "testing" +) + +func TestParseARMDir(t *testing.T) { + b := NewBuilder() + if err := ParseARMDir(b, "testdata/arm"); err != nil { + t.Fatal(err) + } + table := b.Table() + + // sysreg_ and AArch64- files must be skipped entirely. + if _, ok := table["SHOULDNOTAPPEAR"]; ok { + t.Fatal("skipped file leaked into the table") + } + + add, ok := table["ADD"] + if !ok { + t.Fatal("ADD missing") + } + // Brief comes from , not the noisy "-- A64" title attr. + if add.Brief != "Add (immediate)" { + t.Errorf("ADD brief = %q", add.Brief) + } + // Description is the first paragraph only; the second must be dropped. + if !strings.HasPrefix(add.Description, "Add (immediate) adds a register value") { + t.Errorf("ADD description = %q", add.Description) + } + if strings.Contains(add.Description, "second paragraph") { + t.Errorf("ADD description leaked a later paragraph: %q", add.Description) + } + if len(add.Syntax) != 1 || add.Syntax[0] != "ADD , , #" { + t.Errorf("ADD syntax = %#v", add.Syntax) + } + if got := add.Operands[""]; !strings.Contains(got, "destination register") { + t.Errorf("ADD operand = %q", got) + } + if _, ok := add.Operands[""]; !ok { + t.Errorf("ADD operand missing: %#v", add.Operands) + } + + // Two LDR encodings in one file must merge into distinct syntaxes. + ldr := table["LDR"] + if len(ldr.Syntax) != 2 { + t.Errorf("LDR should have 2 syntaxes, got %#v", ldr.Syntax) + } + + // Alias files must be keyed by the alias mnemonic (UBFIZ), not the base + // (UBFM) from the mnemonic docvar. + ubfiz, ok := table["UBFIZ"] + if !ok { + t.Fatal("UBFIZ alias missing") + } + if ubfiz.Brief != "Unsigned Bitfield Insert in Zero" { + t.Errorf("UBFIZ brief = %q", ubfiz.Brief) + } + if len(ubfiz.Syntax) != 1 || !strings.HasPrefix(ubfiz.Syntax[0], "UBFIZ ") { + t.Errorf("UBFIZ syntax = %#v", ubfiz.Syntax) + } + if _, leaked := table["UBFM"]; leaked { + t.Error("alias content leaked into base UBFM key") + } +} diff --git a/internal/asmref/gen/build.go b/internal/asmref/gen/build.go new file mode 100644 index 0000000..2a58f4d --- /dev/null +++ b/internal/asmref/gen/build.go @@ -0,0 +1,88 @@ +package main + +import ( + "regexp" + "slices" + "strings" + + "loov.dev/lensm/internal/asmref" +) + +// Builder accumulates parsed fragments and merges everything that shares a +// mnemonic into a single Entry. Both the ARM and x86 parsers feed the same +// builder so, e.g., an ADD documented in several files collapses to one key. +type Builder struct { + entries map[string]*asmref.Entry +} + +func NewBuilder() *Builder { + return &Builder{entries: map[string]*asmref.Entry{}} +} + +// Add merges one parsed fragment. Merge rules are first-non-empty for the prose +// fields, dedup-append for syntax (document order preserved), and union for +// operands (first meaning for a name wins). This keeps regeneration +// deterministic given the same input. +func (b *Builder) Add(mnemonic, brief, description string, syntax []string, variants []asmref.Variant, operands map[string]string) { + key := strings.ToUpper(strings.TrimSpace(mnemonic)) + if key == "" { + return + } + e := b.entries[key] + if e == nil { + e = &asmref.Entry{Operands: map[string]string{}} + b.entries[key] = e + } + if e.Brief == "" { + e.Brief = clean(brief) + } + if e.Description == "" { + e.Description = clean(description) + } + for _, s := range syntax { + s = normalizeSpace(s) + if s != "" && !slices.Contains(e.Syntax, s) { + e.Syntax = append(e.Syntax, s) + } + } + for _, v := range variants { + if i := slices.IndexFunc(e.Variants, func(x asmref.Variant) bool { return x.Form == v.Form }); i >= 0 { + e.Variants[i].Perf = append(e.Variants[i].Perf, v.Perf...) + } else { + e.Variants = append(e.Variants, v) + } + } + for name, meaning := range operands { + name = strings.TrimSpace(name) + if name != "" && e.Operands[name] == "" { + e.Operands[name] = clean(meaning) + } + } +} + +// Table returns the merged result. Empty operand maps are dropped so the JSON +// stays clean (omitempty handles nil, not empty maps). +func (b *Builder) Table() map[string]asmref.Entry { + out := make(map[string]asmref.Entry, len(b.entries)) + for k, e := range b.entries { + if len(e.Operands) == 0 { + e.Operands = nil + } + out[k] = *e + } + return out +} + +var tagPattern = regexp.MustCompile(`<[^>]+>`) + +// clean strips any residual HTML/XML tags and collapses whitespace to a single +// space. Operand text like "" reaches clean already decoded, so only real +// markup (from description paragraphs) is removed. +func clean(s string) string { + s = tagPattern.ReplaceAllString(s, "") + return normalizeSpace(s) +} + +func normalizeSpace(s string) string { + return strings.Join(strings.Fields(s), " ") +} diff --git a/internal/asmref/gen/coverage.go b/internal/asmref/gen/coverage.go new file mode 100644 index 0000000..13e8e6c --- /dev/null +++ b/internal/asmref/gen/coverage.go @@ -0,0 +1,57 @@ +package main + +import ( + "sort" + "strings" + + "golang.org/x/arch/arm64/arm64asm" + "golang.org/x/arch/x86/x86asm" +) + +// knownMnemonics enumerates every mnemonic x/arch's decoders can name. There is +// no exported count, so we probe Op values until a long run returns the +// "Op(N)" fallback, which marks the end of the name table. +func knownMnemonics() map[string]bool { + out := map[string]bool{} + add := func(s string) { + if s == "" || strings.HasPrefix(s, "Op(") { + return + } + out[strings.ToUpper(s)] = true + } + miss := 0 + for i := 1; i < 65000 && miss < 1000; i++ { + s := x86asm.Op(i).String() + if s == "" || strings.HasPrefix(s, "Op(") { + miss++ + continue + } + miss = 0 + add(s) + } + miss = 0 + for i := 1; i < 65000 && miss < 1000; i++ { + s := arm64asm.Op(i).String() + if s == "" || strings.HasPrefix(s, "Op(") { + miss++ + continue + } + miss = 0 + add(s) + } + return out +} + +// missingCoverage returns mnemonics x/arch knows that the generated table does +// not cover. It is advisory: a gap usually just means an ISA XML file was not +// present, not that anything is wrong. +func missingCoverage(table map[string]bool) []string { + var missing []string + for name := range knownMnemonics() { + if !table[name] { + missing = append(missing, name) + } + } + sort.Strings(missing) + return missing +} diff --git a/internal/asmref/gen/main.go b/internal/asmref/gen/main.go new file mode 100644 index 0000000..d2adf19 --- /dev/null +++ b/internal/asmref/gen/main.go @@ -0,0 +1,69 @@ +// Command gen flattens CPU ISA XML into internal/asmref/table.json.gz. +// +// It is run via `go generate ./internal/asmref`. By default it reads the small +// checked-in fixtures so a bare run stays reproducible; point -arm and -x86 at +// a real ARM AArch64 ISA XML release and a uops.info instructions.xml to +// produce the full table. See ../README.md for source versions and URLs. +package main + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "strings" +) + +func main() { + armDir := flag.String("arm", "gen/testdata/arm", "directory of ARM AArch64 ISA XML files") + x86File := flag.String("x86", "gen/testdata/x86/instructions.xml", "uops.info instructions.xml path") + out := flag.String("out", "table.json.gz", "output path (gzip-compressed JSON)") + flag.Parse() + + b := NewBuilder() + if *armDir != "" { + if err := ParseARMDir(b, *armDir); err != nil { + log.Fatalf("arm: %v", err) + } + } + if *x86File != "" { + if err := ParseX86File(b, *x86File); err != nil { + log.Fatalf("x86: %v", err) + } + } + + table := b.Table() + + covered := make(map[string]bool, len(table)) + for k := range table { + covered[k] = true + } + if missing := missingCoverage(covered); len(missing) > 0 { + sample := missing + if len(sample) > 20 { + sample = sample[:20] + } + fmt.Fprintf(os.Stderr, "asmref: %d mnemonic(s) known to x/arch are not in the table (e.g. %s)\n", + len(missing), strings.Join(sample, ", ")) + } + + data, err := json.Marshal(table) + if err != nil { + log.Fatalf("marshal: %v", err) + } + var buf bytes.Buffer + zw, _ := gzip.NewWriterLevel(&buf, gzip.BestCompression) + if _, err := zw.Write(data); err != nil { + log.Fatalf("gzip: %v", err) + } + if err := zw.Close(); err != nil { + log.Fatalf("gzip: %v", err) + } + if err := os.WriteFile(*out, buf.Bytes(), 0o644); err != nil { + log.Fatalf("write %s: %v", *out, err) + } + fmt.Fprintf(os.Stderr, "asmref: wrote %d mnemonics to %s (%d KB gzipped)\n", len(table), *out, buf.Len()/1024) +} diff --git a/internal/asmref/gen/testdata/arm/AArch64-shared.xml b/internal/asmref/gen/testdata/arm/AArch64-shared.xml new file mode 100644 index 0000000..8052c20 --- /dev/null +++ b/internal/asmref/gen/testdata/arm/AArch64-shared.xml @@ -0,0 +1,9 @@ + + + + + + SHOULDNOTAPPEAR + + + diff --git a/internal/asmref/gen/testdata/arm/abs_advsimd.xml b/internal/asmref/gen/testdata/arm/abs_advsimd.xml new file mode 100644 index 0000000..7ed39b3 --- /dev/null +++ b/internal/asmref/gen/testdata/arm/abs_advsimd.xml @@ -0,0 +1,19 @@ + + + + Absolute value (vector) + + Absolute value (vector) computes the absolute value of each vector element and writes the result to the destination vector. + + + + + + 0 + + + ABS <Vd>, <Vn> + + + + diff --git a/internal/asmref/gen/testdata/arm/add_imm.xml b/internal/asmref/gen/testdata/arm/add_imm.xml new file mode 100644 index 0000000..ed6a036 --- /dev/null +++ b/internal/asmref/gen/testdata/arm/add_imm.xml @@ -0,0 +1,35 @@ + + + + + + + Add (immediate) + + Add (immediate) adds a register value and an optionally-shifted immediate value, and writes the result to the destination register. + This second paragraph must be ignored. + + + + + + + + + 1 + 0 + + + + + + ADD <Wd>, <Wn>, #<imm> + + + + + + if d == 31 then SP[] = result; else X[d] = result; + + + diff --git a/internal/asmref/gen/testdata/arm/ldr_imm.xml b/internal/asmref/gen/testdata/arm/ldr_imm.xml new file mode 100644 index 0000000..e6bbbc9 --- /dev/null +++ b/internal/asmref/gen/testdata/arm/ldr_imm.xml @@ -0,0 +1,30 @@ + + + + Load register (immediate) + + Load Register (immediate) loads a word or doubleword from memory and writes it to a register. + + + + + + + + 1 + + + LDR <Wt>, [<Xn|SP>], #<simm> + + + + + + + + + LDR <Wt>, [<Xn|SP>, #<simm>]! + + + + diff --git a/internal/asmref/gen/testdata/arm/sysreg_ctr_el0.xml b/internal/asmref/gen/testdata/arm/sysreg_ctr_el0.xml new file mode 100644 index 0000000..9dff2c3 --- /dev/null +++ b/internal/asmref/gen/testdata/arm/sysreg_ctr_el0.xml @@ -0,0 +1,9 @@ + + + + + + MRS SHOULDNOTAPPEAR + + + diff --git a/internal/asmref/gen/testdata/arm/ubfiz_ubfm.xml b/internal/asmref/gen/testdata/arm/ubfiz_ubfm.xml new file mode 100644 index 0000000..2626410 --- /dev/null +++ b/internal/asmref/gen/testdata/arm/ubfiz_ubfm.xml @@ -0,0 +1,28 @@ + + + + + + + + + Unsigned Bitfield Insert in Zero + + Unsigned Bitfield Insert in Zero copies a bitfield of a given width to the least significant bits, with the destination bits to the left and right set to zero. + + + + + + + + + 0 + + + UBFIZ <Wd>, <Wn>, #<lsb>, #<width> + + + + diff --git a/internal/asmref/gen/testdata/x86/instructions.xml b/internal/asmref/gen/testdata/x86/instructions.xml new file mode 100644 index 0000000..3809b39 --- /dev/null +++ b/internal/asmref/gen/testdata/x86/instructions.xml @@ -0,0 +1,42 @@ + + + + + EAX,ECX,EDX,EBX + EAX,ECX,EDX,EBX + + + + + + + + + + + + + + + + + EAX,ECX,EDX,EBX + + + + + + + + + + EAX,ECX,EDX,EBX + AL,CL,DL,BL + + + + + + + + diff --git a/internal/asmref/gen/x86.go b/internal/asmref/gen/x86.go new file mode 100644 index 0000000..af271c7 --- /dev/null +++ b/internal/asmref/gen/x86.go @@ -0,0 +1,139 @@ +package main + +import ( + "bufio" + "encoding/xml" + "fmt" + "io" + "os" + "strconv" + "strings" + + "loov.dev/lensm/internal/asmref" +) + +// ParseX86File parses a uops.info instructions.xml and feeds every instruction +// into the builder, keyed by its base mnemonic (the asm attribute). +// +// uops.info is a benchmark dataset, not an ISA manual: there are no or +// elements. The usable "API" lives in attributes — summary (a +// human title, the brief) and string (the operand-form). The children +// are not emitted (their derived descriptions just restate the token already in +// the form). +// +// Performance data is captured per operand form across every measured +// microarchitecture: for each the first yields +// uops, ports, throughput and worst-case latency. estimate nodes are +// skipped in favour of the real measurements. The file is ~140MB, so this is a +// streaming token walk. +func ParseX86File(b *Builder, path string) error { + f, err := os.Open(path) + if err != nil { + return err + } + defer f.Close() + + dec := xml.NewDecoder(bufio.NewReaderSize(f, 1<<20)) + var ( + cur *x86Inst + curArch string + took bool // a measurement was already taken for curArch + building bool // inside the measurement we are keeping + m asmref.ArchPerf + ) + for { + tok, err := dec.Token() + if err == io.EOF { + break + } + if err != nil { + return fmt.Errorf("%s: %w", path, err) + } + switch t := tok.(type) { + case xml.StartElement: + switch t.Name.Local { + case "instruction": + cur = &x86Inst{asm: attr(t, "asm"), summary: attr(t, "summary"), form: attr(t, "string")} + case "architecture": + curArch, took = attr(t, "name"), false + case "IACA": + _ = dec.Skip() // estimate, not a measurement + case "measurement": + if cur == nil || curArch == "" || took { + _ = dec.Skip() + break + } + building = true + m = asmref.ArchPerf{ + Arch: curArch, + Uops: atoi(attr(t, "uops")), + Ports: strings.TrimSpace(attr(t, "ports")), + TP: measuredThroughput(t), + } + case "latency": + if building { + if c := atoi(attr(t, "cycles")); c > m.Latency { + m.Latency = c + } + } + } + case xml.EndElement: + switch t.Name.Local { + case "measurement": + if building { + cur.perf = append(cur.perf, m) + building, took = false, true + } + case "instruction": + if cur != nil { + addX86(b, cur) + cur = nil + } + } + } + } + return nil +} + +type x86Inst struct { + asm string + summary string + form string + perf []asmref.ArchPerf +} + +func addX86(b *Builder, inst *x86Inst) { + mnemonic := strings.ToUpper(strings.TrimSpace(inst.asm)) + if mnemonic == "" { + return + } + form := strings.TrimSpace(inst.form) + var syntax []string + var variants []asmref.Variant + if form != "" { + syntax = []string{form} + if len(inst.perf) > 0 { + variants = []asmref.Variant{{Form: form, Perf: inst.perf}} + } + } + // summary is the only human text; it serves as the brief. + b.Add(mnemonic, inst.summary, "", syntax, variants, nil) +} + +// measuredThroughput prefers the loop-measured throughput, falling back to the +// unrolled measurement. +func measuredThroughput(t xml.StartElement) float64 { + for _, key := range []string{"TP_loop", "TP_unrolled", "TP"} { + if v := attr(t, key); v != "" { + if f, err := strconv.ParseFloat(v, 64); err == nil { + return f + } + } + } + return 0 +} + +func atoi(s string) int { + n, _ := strconv.Atoi(strings.TrimSpace(s)) + return n +} diff --git a/internal/asmref/gen/x86_test.go b/internal/asmref/gen/x86_test.go new file mode 100644 index 0000000..4b1100e --- /dev/null +++ b/internal/asmref/gen/x86_test.go @@ -0,0 +1,89 @@ +package main + +import ( + "strings" + "testing" + + "loov.dev/lensm/internal/asmref" +) + +func TestParseX86File(t *testing.T) { + b := NewBuilder() + if err := ParseX86File(b, "testdata/x86/instructions.xml"); err != nil { + t.Fatal(err) + } + table := b.Table() + + add, ok := table["ADD"] + if !ok { + t.Fatal("ADD missing") + } + if add.Brief != "Add" { + t.Errorf("ADD brief = %q", add.Brief) + } + // Both operand forms merge as syntax and as perf variants. + wantSyntax := []string{"ADD (R32, R32)", "ADD (M32, R32)"} + if strings.Join(add.Syntax, "|") != strings.Join(wantSyntax, "|") { + t.Errorf("ADD syntax = %#v", add.Syntax) + } + if len(add.Variants) != 2 { + t.Fatalf("ADD should have 2 variants, got %d", len(add.Variants)) + } + + // First form carries all measured microarchitectures (IACA nodes ignored). + regReg := findVariant(add, "ADD (R32, R32)") + if regReg == nil { + t.Fatal("ADD (R32, R32) variant missing") + } + if got := archNames(regReg.Perf); strings.Join(got, ",") != "ADL-P,ZEN5" { + t.Errorf("ADD (R32, R32) arches = %v", got) + } + adl := findPerf(regReg.Perf, "ADL-P") + if adl == nil || adl.Uops != 1 || adl.Ports != "1*p0156" || adl.Latency != 1 || adl.TP != 0.25 { + t.Errorf("ADD (R32, R32) ADL-P perf = %#v", adl) + } + + // The memory form has higher latency and load ports. + memReg := findVariant(add, "ADD (M32, R32)") + adlMem := findPerf(memReg.Perf, "ADL-P") + if adlMem == nil || adlMem.Latency != 6 || adlMem.Uops != 2 { + t.Errorf("ADD (M32, R32) ADL-P perf = %#v", adlMem) + } + + crc, ok := table["CRC32"] + if !ok { + t.Fatal("CRC32 missing") + } + if crc.Brief != "Accumulate CRC32 Value" { + t.Errorf("CRC32 brief = %q", crc.Brief) + } + if p := crc.PerfFor("ADL-P"); len(p) != 1 || p[0].Latency != 3 { + t.Errorf("CRC32 ADL-P perf = %#v", p) + } +} + +func findVariant(e asmref.Entry, form string) *asmref.Variant { + for i := range e.Variants { + if e.Variants[i].Form == form { + return &e.Variants[i] + } + } + return nil +} + +func findPerf(perf []asmref.ArchPerf, arch string) *asmref.ArchPerf { + for i := range perf { + if perf[i].Arch == arch { + return &perf[i] + } + } + return nil +} + +func archNames(perf []asmref.ArchPerf) []string { + var out []string + for _, p := range perf { + out = append(out, p.Arch) + } + return out +} diff --git a/internal/asmref/table.json.gz b/internal/asmref/table.json.gz new file mode 100644 index 0000000..95e9798 Binary files /dev/null and b/internal/asmref/table.json.gz differ diff --git a/internal/atomicfile/atomicfile.go b/internal/atomicfile/atomicfile.go new file mode 100644 index 0000000..b627804 --- /dev/null +++ b/internal/atomicfile/atomicfile.go @@ -0,0 +1,53 @@ +package atomicfile + +import ( + "os" + "path/filepath" +) + +// atomicWriteFile replaces path with data via a temp file in the same +// directory: the temp file is chmodded (CreateTemp creates it 0600), +// fsynced so a crash cannot leave a truncated file behind the +// already-journaled rename, renamed over the target, and the parent +// directory is best-effort synced so the rename itself survives a crash. +func Write(path string, data []byte, perm os.FileMode) error { + dir := filepath.Dir(path) + tmp, err := os.CreateTemp(dir, "."+filepath.Base(path)+"-*.tmp") + if err != nil { + return err + } + tmpName := tmp.Name() + defer os.Remove(tmpName) + + if err := tmp.Chmod(perm); err != nil { + _ = tmp.Close() + return err + } + if _, err := tmp.Write(data); err != nil { + _ = tmp.Close() + return err + } + if err := tmp.Sync(); err != nil { + _ = tmp.Close() + return err + } + if err := tmp.Close(); err != nil { + return err + } + if err := os.Rename(tmpName, path); err != nil { + return err + } + syncDir(dir) + return nil +} + +// syncDir best-effort flushes directory metadata so a rename survives a +// crash. Errors are ignored: not all platforms support syncing directories. +func syncDir(dir string) { + d, err := os.Open(dir) + if err != nil { + return + } + _ = d.Sync() + _ = d.Close() +} diff --git a/internal/codeview/codeview.go b/internal/codeview/codeview.go new file mode 100644 index 0000000..9990991 --- /dev/null +++ b/internal/codeview/codeview.go @@ -0,0 +1,152 @@ +package codeview + +import ( + "image" + "strings" + + "gioui.org/f32" + "gioui.org/io/pointer" + "gioui.org/layout" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + + "loov.dev/lensm/internal/comments" + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/gui" + "loov.dev/lensm/internal/syntax" +) + +type UI struct { + *disasm.Code + + asm gui.ScrollRegion + src gui.ScrollRegion + + hl highlightCache + + mousePosition f32.Point + SelectedAsm int + SelectedView View + SelectedFile string + SelectedLine int + Selection TextSelection + + selecting bool + selectionPointer pointer.ID + selectionStart f32.Point + selectionMoved bool +} + +// highlightCache holds the syntax highlight spans for a Code. Layout +// runs every frame; highlighting is a pure function of the immutable +// code and the palette, so it is rebuilt only when either changes — +// re-highlighting per frame allocates a go/scanner per source line. +type highlightCache struct { + code *disasm.Code + syntax syntax.Palette + + asm [][]syntax.Span + nativeText []string + native [][]syntax.Span + // source is indexed by source file, block, and line within the block. + source [][][][]syntax.Span +} + +func (hl *highlightCache) update(code *disasm.Code, palette syntax.Palette) { + if hl.code == code && hl.syntax == palette { + return + } + hl.code = code + hl.syntax = palette + + hl.asm = make([][]syntax.Span, len(code.Insts)) + hl.nativeText = make([]string, len(code.Insts)) + hl.native = make([][]syntax.Span, len(code.Insts)) + for i := range code.Insts { + ix := &code.Insts[i] + hl.asm[i] = syntax.HighlightAsm(ix.Text, ix.Call, palette) + hl.nativeText[i] = strings.ToUpper(ix.NativeText) + hl.native[i] = syntax.HighlightAsm(hl.nativeText[i], "", palette) + } + + hl.source = make([][][][]syntax.Span, len(code.Source)) + for i, src := range code.Source { + blocks := make([][][]syntax.Span, len(src.Blocks)) + for j, block := range src.Blocks { + lines := make([][]syntax.Span, len(block.Lines)) + for k, line := range block.Lines { + lines[k] = syntax.HighlightSource(block.From+k, line, palette) + } + blocks[j] = lines + } + hl.source[i] = blocks + } +} + +func (ui *UI) Loaded() bool { + return ui.Code != nil +} + +func (ui *UI) ResetScroll() { + ui.asm.Offset = 100000 + ui.src.Offset = 100000 +} + +type Style struct { + *UI + + TryOpen func(gtx layout.Context, funcname string) + CopyText func(gtx layout.Context, text string) + // OnInteract fires on a primary press in the content, used to keep a + // preview tab open once the user acts on it. + OnInteract func() + + // Comments backs inline comment display and editing. Reads go through + // the store directly; SetComment records an edit (buffered write plus + // flush scheduling), and CommentKey/CommentEditor drive the single + // shared inline editor. + Comments *comments.Store + SetComment func(comments.Coord, string) + CommentKey *string + CommentEditor *widget.Editor + Theme *gui.Theme + Syntax syntax.Palette + + ShowNative bool + ShowHelp bool + TextHeight unit.Sp +} + +func (ui Style) Layout(gtx layout.Context) layout.Dimensions { + gtx.Constraints = layout.Exact(gtx.Constraints.Max) + if ui.Code == nil { + return layout.Dimensions{Size: gtx.Constraints.Max} + } + ui.UI.hl.update(ui.Code, ui.Syntax) + + paint.FillShape(gtx.Ops, ui.Theme.Colors.Background, clip.Rect{Max: gtx.Constraints.Max}.Op()) + defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() + + c := ui.columns(gtx) + mouseClicked := ui.handleInput(gtx, c) + + // draw gutter + paint.FillShape(gtx.Ops, ui.Theme.Colors.Gutter, clip.Rect{ + Min: image.Pt(int(c.gutter.Min), 0), + Max: image.Pt(int(c.gutter.Max), gtx.Constraints.Max.Y), + }.Op()) + if scroll, ok := ui.asm.Anim.Update(gtx); ok { + ui.asm.Offset = scroll + } + + hover := ui.resolveHover(gtx, c, mouseClicked) + highlightRanges := ui.layoutRelations(gtx, c, hover) + ui.layoutAssembly(gtx, c, hover, highlightRanges) + sourceContentHeight := ui.layoutSource(gtx, c, hover, mouseClicked) + ui.layoutScrollbars(gtx, c, sourceContentHeight) + ui.layoutHelp(gtx, c, hover) + + return layout.Dimensions{Size: gtx.Constraints.Max} +} diff --git a/internal/codeview/codeview_test.go b/internal/codeview/codeview_test.go new file mode 100644 index 0000000..ee6f3bd --- /dev/null +++ b/internal/codeview/codeview_test.go @@ -0,0 +1,111 @@ +package codeview + +import ( + "image" + "testing" + "time" + + "gioui.org/f32" + "gioui.org/io/input" + "gioui.org/io/key" + "gioui.org/io/pointer" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/text" + "gioui.org/unit" + "gioui.org/widget/material" + + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/gui" + "loov.dev/lensm/internal/syntax" +) + +func TestCodeUIStyleLayoutWithHelpAndSelection(t *testing.T) { + base := material.NewTheme() + base.Shaper = text.NewShaper(text.WithCollection(gui.LoadFonts(""))) + base.TextSize = unit.Sp(12) + theme := gui.NewTheme(base, false) + state := &UI{Code: &disasm.Code{ + Name: "main.example", + File: "main.go", + MaxJump: 1, + Insts: []disasm.Inst{ + {Text: "MOV (R2), R1", NativeText: "mov (%r2), %r1"}, + {Text: "ADDQ $1, R1", NativeText: "addq $1, %r1"}, + }, + Source: []disasm.Source{{ + File: "main.go", + Blocks: []disasm.SourceBlock{{ + LineRange: disasm.LineRange{From: 1, To: 2}, + Lines: []string{"func example() {", "}"}, + }}, + }}, + }} + state.mousePosition = f32.Pt(100, 22) + + var router input.Router + var operations op.Ops + newContext := func() layout.Context { + operations.Reset() + return layout.Context{ + Ops: &operations, + Source: router.Source(), + Metric: unit.Metric{PxPerDp: 1, PxPerSp: 1}, + Now: time.Now(), + Constraints: layout.Exact(image.Pt(1200, 600)), + } + } + copied := "" + style := Style{ + UI: state, + Theme: theme, + Syntax: syntax.PaletteFor(syntax.StyleGoLand, theme.Colors.SyntaxColors()), + ShowNative: true, + TextHeight: theme.TextSize, + CopyText: func(_ layout.Context, text string) { + copied = text + }, + } + gtx := newContext() + if got := style.Layout(gtx).Size; got != gtx.Constraints.Max { + t.Fatalf("Layout size = %v, want %v", got, gtx.Constraints.Max) + } + router.Frame(gtx.Ops) + + router.Queue( + pointer.Event{Source: pointer.Mouse, PointerID: 1, Buttons: pointer.ButtonPrimary, Kind: pointer.Press, Position: f32.Pt(100, 20)}, + pointer.Event{Source: pointer.Mouse, PointerID: 1, Buttons: pointer.ButtonPrimary, Kind: pointer.Move, Position: f32.Pt(100, 40)}, + pointer.Event{Source: pointer.Mouse, PointerID: 1, Buttons: pointer.ButtonPrimary, Kind: pointer.Release, Position: f32.Pt(100, 40)}, + ) + gtx = newContext() + style.Layout(gtx) + router.Frame(gtx.Ops) + if from, to, ok := state.Selection.Range(); !ok || from != 0 || to != 1 || state.Selection.View != ViewGoAsm { + t.Fatalf("drag selection = %#v, range %d..%d", state.Selection, from, to) + } + if !gtx.Focused(state) { + t.Fatal("code view did not retain keyboard focus after drag selection") + } + + router.Queue(key.Event{Name: key.Name("C"), Modifiers: key.ModShortcut, State: key.Press}) + gtx = newContext() + style.Layout(gtx) + router.Frame(gtx.Ops) + if want := "MOV (R2), R1\nADDQ $1, R1\n"; copied != want { + t.Fatalf("copied text = %q, want %q", copied, want) + } + + // A fast drag past the bottom edge must clamp to the last line, not + // stop at the last in-range drag sample. + router.Queue( + pointer.Event{Source: pointer.Mouse, PointerID: 2, Buttons: pointer.ButtonPrimary, Kind: pointer.Press, Position: f32.Pt(100, 20)}, + pointer.Event{Source: pointer.Mouse, PointerID: 2, Buttons: pointer.ButtonPrimary, Kind: pointer.Move, Position: f32.Pt(100, 5000)}, + pointer.Event{Source: pointer.Mouse, PointerID: 2, Buttons: pointer.ButtonPrimary, Kind: pointer.Release, Position: f32.Pt(100, 5000)}, + ) + gtx = newContext() + style.Layout(gtx) + router.Frame(gtx.Ops) + if from, to, ok := state.Selection.Range(); !ok || from != 0 || to != len(state.Code.Insts)-1 { + t.Fatalf("overshoot drag selection = %#v, range %d..%d", state.Selection, from, to) + } +} diff --git a/internal/codeview/comments.go b/internal/codeview/comments.go new file mode 100644 index 0000000..b4709b2 --- /dev/null +++ b/internal/codeview/comments.go @@ -0,0 +1,104 @@ +package codeview + +import ( + "image" + "strconv" + + "gioui.org/layout" + "gioui.org/op" + "gioui.org/widget" + "gioui.org/widget/material" + + "loov.dev/lensm/internal/comments" + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/gui" +) + +func (ui Style) asmCoord(view View, inst disasm.Inst) comments.Coord { + name := "" + if ui.Code != nil { + name = ui.Code.Name + } + cview, _ := view.CommentView() + return comments.Coord{Function: name, View: cview, PC: inst.PC} +} + +func (ui Style) sourceCoord(file string, line int) comments.Coord { + name := "" + if ui.Code != nil { + name = ui.Code.Name + } + return comments.Coord{Function: name, View: comments.ViewSource, File: file, Line: line} +} + +// commentEditKey identifies which comment the shared inline editor is +// bound to, so moving to a different line reloads the editor text. +func commentEditKey(coord comments.Coord) string { + if coord.View == comments.ViewSource { + return string(coord.View) + ":" + coord.Function + ":" + coord.File + ":" + strconv.Itoa(coord.Line) + } + return string(coord.View) + ":" + coord.Function + ":" + comments.FormatPC(coord.PC) +} + +// layoutInlineCommentEditor draws the comment for coord, either as an +// editable field (when the line is selected and editing is wired up) or +// read-only. prefix is the leading marker: ";" for assembly, "//" for +// source. +func (ui Style) layoutInlineCommentEditor(gtx layout.Context, coord comments.Coord, prefix string, top, left, width, lineHeight int) { + if ui.CommentEditor == nil || ui.CommentKey == nil || ui.SetComment == nil { + comment := ui.Comments.Get(coord) + if comment == "" { + return + } + gui.SourceLine{ + TopLeft: image.Pt(left, top), + Width: width, + Text: prefix + " " + comment, + TextHeight: ui.TextHeight, + Italic: true, + Color: ui.Theme.Colors.MutedText, + }.Layout(ui.Theme.Theme, gtx) + return + } + + key := commentEditKey(coord) + if key != *ui.CommentKey { + *ui.CommentKey = key + ui.CommentEditor.SetText(ui.Comments.Get(coord)) + } + + changed := false + for { + ev, ok := ui.CommentEditor.Update(gtx) + if !ok { + break + } + switch ev.(type) { + case widget.ChangeEvent, widget.SubmitEvent: + changed = true + } + } + if changed { + ui.SetComment(coord, ui.CommentEditor.Text()) + } + + prefixWidth := lineHeight + gui.SourceLine{ + TopLeft: image.Pt(left, top), + Width: prefixWidth, + Text: prefix, + TextHeight: ui.TextHeight, + Color: ui.Theme.Colors.MutedText, + }.Layout(ui.Theme.Theme, gtx) + + editorLeft := left + prefixWidth + editorWidth := max(width-prefixWidth, 0) + stack := op.Offset(image.Pt(editorLeft, top)).Push(gtx.Ops) + gtx.Constraints = layout.Exact(image.Pt(editorWidth, lineHeight)) + editor := material.Editor(ui.Theme.Theme, ui.CommentEditor, "comment") + editor.TextSize = ui.TextHeight + editor.Font.Typeface = "override-monospace,Go,monospace" + editor.Color = ui.Theme.Colors.Text + editor.Layout(gtx) + stack.Pop() +} diff --git a/internal/codeview/draw.go b/internal/codeview/draw.go new file mode 100644 index 0000000..15cc82e --- /dev/null +++ b/internal/codeview/draw.go @@ -0,0 +1,343 @@ +package codeview + +import ( + "image" + "math" + + "gioui.org/f32" + "gioui.org/io/key" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/f32color" + "loov.dev/lensm/internal/gui" +) + +// layoutRelations draws the curved bands linking source lines to the +// assembly ranges they compiled to, and returns the assembly ranges under +// the pointer so the jump lines can highlight them. +func (ui Style) layoutRelations(gtx layout.Context, c codeColumns, hover codeHover) []disasm.LineRange { + lineHeight := c.lineHeight + gutter, source, asm := c.gutter, c.source, c.asm + mousePosition := hover.position + mouseInAsm, mouseInSource := hover.inAsm, hover.inSource + + var highlightRanges []disasm.LineRange + top := int(ui.src.Offset) + var highlightPaths []clip.PathSpec + relationStroke := ui.Theme.Colors.RelationStroke + relationFill := relationStroke + relationFill.A /= 2 + for i, src := range ui.Code.Source { + if i > 0 { + top += lineHeight + } + top += lineHeight + for i, block := range src.Blocks { + if i > 0 { + top += lineHeight + } + for _, ranges := range block.Related { + if len(ranges) > 0 { + highlight := mouseInSource && float32(top) <= mousePosition.Y && mousePosition.Y < float32(top+lineHeight) + if !highlight && mouseInAsm { + for _, r := range ranges { + if float32(r.From*lineHeight)+ui.asm.Offset <= mousePosition.Y && mousePosition.Y < float32(r.To*lineHeight)+ui.asm.Offset { + highlight = true + break + } + } + } + if highlight { + highlightRanges = ranges + + var p clip.Path + p.Begin(gtx.Ops) + p.MoveTo(f32.Pt(gutter.Max, float32(top+lineHeight))) + p.LineTo(f32.Pt(source.Max, float32(top+lineHeight))) + p.LineTo(f32.Pt(source.Max, float32(top))) + p.LineTo(f32.Pt(gutter.Max, float32(top))) + pin := float32(top) + for i, r := range ranges { + const S = 0.1 + p.CubeTo( + f32.Pt(gutter.Lerp(0.5-S), pin), + f32.Pt(gutter.Lerp(0.5+S), float32(r.From*lineHeight)+ui.asm.Offset), + f32.Pt(gutter.Min, float32(r.From*lineHeight)+ui.asm.Offset)) + p.LineTo(f32.Pt(asm.Min, float32(r.From*lineHeight)+ui.asm.Offset)) + p.LineTo(f32.Pt(asm.Min, float32(r.To*lineHeight)+ui.asm.Offset)) + p.LineTo(f32.Pt(gutter.Min, float32(r.To*lineHeight)+ui.asm.Offset)) + pin = float32(top) + float32(lineHeight)*float32(i+1)/float32(len(ranges)) + p.CubeTo( + f32.Pt(gutter.Lerp(0.5+S), float32(r.To*lineHeight)+ui.asm.Offset), + f32.Pt(gutter.Lerp(0.5-S), pin), + f32.Pt(gutter.Max, pin)) + } + highlightPaths = append(highlightPaths, p.End()) + } + } + top += lineHeight + } + } + } + for _, path := range highlightPaths { + paint.FillShape(gtx.Ops, relationFill, clip.Outline{Path: path}.Op()) + paint.FillShape(gtx.Ops, relationStroke, clip.Stroke{Path: path, Width: 1}.Op()) + } + return highlightRanges +} + +// layoutAssembly draws the Go and native assembly columns: selection +// highlights, instruction text, inline comments, and jump lines. +func (ui Style) layoutAssembly(gtx layout.Context, c codeColumns, hover codeHover, highlightRanges []disasm.LineRange) { + hl := &ui.UI.hl + lineHeight := c.lineHeight + pad, jumpStep := c.pad, c.jumpStep + jump, asm, native, gutter := c.jump, c.asm, c.native, c.gutter + highlightAsmIndex := hover.asmIndex + + asmClip := clip.Rect{ + Min: image.Pt(int(jump.Min), 0), + Max: image.Pt(int(gutter.Min), gtx.Constraints.Max.Y), + }.Push(gtx.Ops) + if ui.ShowNative { + paint.FillShape(gtx.Ops, ui.Theme.Colors.Splitter, clip.Rect{ + Min: image.Pt(int(native.Min)-pad/2, 0), + Max: image.Pt(int(native.Min)-pad/2+1, gtx.Constraints.Max.Y), + }.Op()) + } + for i, ix := range ui.Code.Insts { + if ui.Selection.Contains(ViewGoAsm, i) { + paint.FillShape(gtx.Ops, ui.Theme.Colors.Selection, clip.Rect{ + Min: image.Pt(int(asm.Min), i*lineHeight+int(ui.asm.Offset)), + Max: image.Pt(int(asm.Max), (i+1)*lineHeight+int(ui.asm.Offset)), + }.Op()) + } + if ui.ShowNative && ui.Selection.Contains(ViewNativeAsm, i) { + paint.FillShape(gtx.Ops, ui.Theme.Colors.Selection, clip.Rect{ + Min: image.Pt(int(native.Min), i*lineHeight+int(ui.asm.Offset)), + Max: image.Pt(int(native.Max), (i+1)*lineHeight+int(ui.asm.Offset)), + }.Op()) + } + if ui.SelectedAsm == i { + paint.FillShape(gtx.Ops, ui.Theme.Colors.Selection, clip.Rect{ + Min: image.Pt(int(asm.Min), i*lineHeight+int(ui.asm.Offset)), + Max: image.Pt(int(gutter.Min), (i+1)*lineHeight+int(ui.asm.Offset)), + }.Op()) + } + gui.SourceLine{ + TopLeft: image.Pt(c.goTextLeft, i*lineHeight+int(ui.asm.Offset)), + Width: c.goInstructionWidth, + Text: ix.Text, + Spans: hl.asm[i], + TextHeight: ui.TextHeight, + Italic: ix.Call != "", + Bold: highlightAsmIndex == i || ui.SelectedAsm == i, + Color: ui.Syntax.Plain, + }.Layout(ui.Theme.Theme, gtx) + if c.commentWidth > 0 && ix.Text != "" { + comment := ui.Comments.Get(ui.asmCoord(ViewGoAsm, ix)) + if ui.SelectedAsm == i && ui.SelectedView == ViewGoAsm { + ui.layoutInlineCommentEditor(gtx, ui.asmCoord(ViewGoAsm, ix), ";", i*lineHeight+int(ui.asm.Offset), c.commentLeft, c.commentWidth, lineHeight) + } else if comment != "" { + gui.SourceLine{ + TopLeft: image.Pt(c.commentLeft, i*lineHeight+int(ui.asm.Offset)), + Width: c.commentWidth, + Text: "; " + comment, + TextHeight: ui.TextHeight, + Italic: true, + Color: ui.Theme.Colors.MutedText, + }.Layout(ui.Theme.Theme, gtx) + } + } + if ui.ShowNative { + nativeComment := ui.Comments.Get(ui.asmCoord(ViewNativeAsm, ix)) + width := c.nativeTextWidth + if (nativeComment != "" || (ui.SelectedAsm == i && ui.SelectedView == ViewNativeAsm)) && c.nativeCommentWidth > 0 { + width = c.nativeInstructionWidth + } + gui.SourceLine{ + TopLeft: image.Pt(c.nativeTextLeft, i*lineHeight+int(ui.asm.Offset)), + Width: width, + Text: hl.nativeText[i], + Spans: hl.native[i], + TextHeight: ui.TextHeight, + Bold: highlightAsmIndex == i || ui.SelectedAsm == i, + Color: ui.Syntax.Plain, + }.Layout(ui.Theme.Theme, gtx) + if ui.SelectedAsm == i && ui.SelectedView == ViewNativeAsm && c.nativeCommentWidth > 0 { + ui.layoutInlineCommentEditor(gtx, ui.asmCoord(ViewNativeAsm, ix), ";", i*lineHeight+int(ui.asm.Offset), c.nativeCommentLeft, c.nativeCommentWidth, lineHeight) + } else if nativeComment != "" && c.nativeCommentWidth > 0 { + gui.SourceLine{ + TopLeft: image.Pt(c.nativeCommentLeft, i*lineHeight+int(ui.asm.Offset)), + Width: c.nativeCommentWidth, + Text: "; " + nativeComment, + TextHeight: ui.TextHeight, + Italic: true, + Color: ui.Theme.Colors.MutedText, + }.Layout(ui.Theme.Theme, gtx) + } + } + + // jump line + if ix.RefOffset != 0 { + lineWidth := gtx.Metric.Dp(1) + align := float32(lineWidth%2) / 2 + stack := op.Affine(f32.Affine2D{}.Offset( + f32.Pt(jump.Max+align, float32(i*lineHeight)+align+ui.asm.Offset))).Push(gtx.Ops) + + var path clip.Path + path.Begin(gtx.Ops) + path.MoveTo(f32.Pt(float32(pad/2), float32(lineHeight*2/3))) + path.LineTo(f32.Pt(float32(-jumpStep*ix.RefStack), float32(lineHeight*2/3))) + path.LineTo(f32.Pt(float32(-jumpStep*ix.RefStack), float32(lineHeight/3+ix.RefOffset*lineHeight))) + path.LineTo(f32.Pt(float32(-jumpStep/2), float32(lineHeight/3+ix.RefOffset*lineHeight))) + // draw arrow + path.Line(f32.Pt(0, float32(lineHeight/4))) + path.Line(f32.Pt(float32(lineHeight/3), float32(-lineHeight/4))) + path.Line(f32.Pt(float32(-lineHeight/3), float32(-lineHeight/4))) + path.Line(f32.Pt(0, float32(lineHeight/4))) + + width := float32(lineWidth) + alpha := float32(0.7) + if highlightAsmIndex >= 0 && (highlightAsmIndex == i || highlightAsmIndex == i+ix.RefOffset) { + width *= 3 + alpha = 1 + } else if disasm.LineRangesContain(highlightRanges, i, i+ix.RefOffset) { + width *= 3 + } + jumpColor := f32color.HSLA(float32(math.Mod(float64(ix.PC)*math.Phi, 1)), 0.8, 0.4, alpha) + paint.FillShape(gtx.Ops, jumpColor, clip.Stroke{Path: path.End(), Width: width}.Op()) + + stack.Pop() + } + } + asmClip.Pop() +} + +// layoutSource draws the source column: file headers, source lines, +// selection highlights, and inline comments. It returns the total pixel +// height of the source content for the scrollbar. +func (ui Style) layoutSource(gtx layout.Context, c codeColumns, hover codeHover, mouseClicked bool) int { + hl := &ui.UI.hl + lineHeight := c.lineHeight + source := c.source + mousePosition := hover.position + mouseInSource := hover.inSource + + sourceClip := clip.Rect{ + Min: image.Pt(int(source.Min), 0), + Max: image.Pt(int(source.Max), gtx.Constraints.Max.Y), + }.Push(gtx.Ops) + top := int(ui.src.Offset) + sourceRow := 0 + paintSourceSelection := func(row, rowTop int) { + if ui.Selection.Contains(ViewSource, row) { + paint.FillShape(gtx.Ops, ui.Theme.Colors.Selection, clip.Rect{ + Min: image.Pt(int(source.Min), rowTop), + Max: image.Pt(int(source.Max), rowTop+lineHeight), + }.Op()) + } + } + for i, src := range ui.Code.Source { + if i > 0 { + paintSourceSelection(sourceRow, top) + top += lineHeight + sourceRow++ + } + paintSourceSelection(sourceRow, top) + gui.SourceLine{ + TopLeft: image.Pt(int(source.Min), top), + Text: src.File, + TextHeight: ui.TextHeight, + Bold: hover.asmIndex == i, + Color: ui.Theme.Colors.MutedText, + }.Layout(ui.Theme.Theme, gtx) + top += lineHeight + sourceRow++ + for blockIndex, block := range src.Blocks { + if blockIndex > 0 { + paintSourceSelection(sourceRow, top) + top += lineHeight + sourceRow++ + } + for off := range block.Lines { + paintSourceSelection(sourceRow, top) + highlight := mouseInSource && float32(top) <= mousePosition.Y && mousePosition.Y < float32(top+lineHeight) + lineNo := block.From + off + if highlight && mouseClicked { + ui.SelectedAsm = -1 + ui.SelectedView = ViewSource + ui.SelectedFile = src.File + ui.SelectedLine = lineNo + if ui.CommentEditor != nil { + gtx.Execute(key.FocusCmd{Tag: ui.CommentEditor}) + } + } + sourceComment := ui.Comments.Get(ui.sourceCoord(src.File, lineNo)) + width := c.sourceTextWidth + selectedSource := ui.SelectedView == ViewSource && ui.SelectedFile == src.File && ui.SelectedLine == lineNo + if (sourceComment != "" || selectedSource) && c.sourceCommentWidth > 0 { + width = c.sourceCodeWidth + } + gui.SourceLine{ + TopLeft: image.Pt(int(source.Min), top), + Width: width, + Spans: hl.source[i][blockIndex][off], + TextHeight: ui.TextHeight, + Bold: highlight, + Color: ui.Syntax.Plain, + }.Layout(ui.Theme.Theme, gtx) + if selectedSource && c.sourceCommentWidth > 0 { + ui.layoutInlineCommentEditor(gtx, ui.sourceCoord(src.File, lineNo), "//", top, c.sourceCommentLeft, c.sourceCommentWidth, lineHeight) + } else if sourceComment != "" && c.sourceCommentWidth > 0 { + gui.SourceLine{ + TopLeft: image.Pt(c.sourceCommentLeft, top), + Width: c.sourceCommentWidth, + Text: "// " + sourceComment, + TextHeight: ui.TextHeight, + Italic: true, + Color: ui.Theme.Colors.MutedText, + }.Layout(ui.Theme.Theme, gtx) + } + top += lineHeight + sourceRow++ + } + } + } + sourceClip.Pop() + return top - int(ui.src.Offset) +} + +// layoutScrollbars draws and services the assembly and source scrollbars, +// clamping each column's scroll offset to its content. +func (ui Style) layoutScrollbars(gtx layout.Context, c codeColumns, sourceContentHeight int) { + lineHeight := c.lineHeight + pad := c.pad + jump, gutter, source := c.jump, c.gutter, c.source + + overflow := float32(lineHeight) + + { + stack := clip.Rect{ + Min: image.Pt(int(jump.Min)-pad, 0), + Max: image.Pt(int(gutter.Min), gtx.Constraints.Max.Y), + }.Push(gtx.Ops) + ui.asm.LayoutBar(gtx, ui.Theme.Theme, int(jump.Min)-pad, pad, + -overflow, float32(len(ui.Code.Insts)*lineHeight)+overflow) + stack.Pop() + } + + { + stack := clip.Rect{ + Min: image.Pt(int(source.Min), 0), + Max: image.Pt(int(source.Max)+pad, gtx.Constraints.Max.Y), + }.Push(gtx.Ops) + ui.src.LayoutBar(gtx, ui.Theme.Theme, int(source.Max), pad, + -overflow, float32(sourceContentHeight)+overflow) + stack.Pop() + } +} diff --git a/internal/codeview/geometry.go b/internal/codeview/geometry.go new file mode 100644 index 0000000..8947289 --- /dev/null +++ b/internal/codeview/geometry.go @@ -0,0 +1,124 @@ +package codeview + +import ( + "gioui.org/f32" + "gioui.org/layout" + + "loov.dev/lensm/internal/gui" +) + +// codeColumns is the horizontal geometry of the code view: the pixel +// bounds of each column and the derived text/comment sub-regions. It all +// derives from the viewport width, line height, and whether the native +// assembly column is shown. +type codeColumns struct { + lineHeight int + pad int + jumpStep int + + jump gui.Bounds + asm gui.Bounds + native gui.Bounds + gutter gui.Bounds + source gui.Bounds + + goTextLeft int + goInstructionWidth int + commentLeft int + commentWidth int + + nativeTextLeft int + nativeTextWidth int + nativeCommentLeft int + nativeCommentWidth int + nativeInstructionWidth int + + sourceTextLeft int + sourceTextWidth int + sourceCommentLeft int + sourceCommentWidth int + sourceCodeWidth int +} + +// codeHover is the transient pointer state for one frame: where the mouse +// is and which instruction, if any, it hovers. +type codeHover struct { + position f32.Point + inAsm bool + inSource bool + asmIndex int +} + +func (ui Style) columns(gtx layout.Context) codeColumns { + // The layout has the following sections: + // pad | Jump | pad/2 | Go asm | pad | Native asm | pad | Gutter | pad | Source | pad + lineHeight := gui.CodeLineHeightPx(gtx, ui.TextHeight) + pad := lineHeight + jumpStep := lineHeight / 2 + jumpWidth := jumpStep * ui.Code.MaxJump + gutterWidth := lineHeight * 8 + fixedWidth := gutterWidth + jumpWidth + 4*pad + pad/2 + if ui.ShowNative { + fixedWidth += pad + } + blocksWidth := max(0, gtx.Constraints.Max.X-fixedWidth) + + jump := gui.BoundsWidth(pad, jumpWidth) + asmWidth := blocksWidth * 40 / 100 + if ui.ShowNative { + asmWidth = blocksWidth * 28 / 100 + } + asm := gui.BoundsWidth(int(jump.Max)+pad/2, asmWidth) + native := gui.BoundsWidth(int(asm.Max), 0) + gutter := gui.BoundsWidth(int(asm.Max)+pad, gutterWidth) + sourceWidth := blocksWidth - int(asm.Width()) + if ui.ShowNative { + native = gui.BoundsWidth(int(asm.Max)+pad, blocksWidth*28/100) + gutter = gui.BoundsWidth(int(native.Max)+pad, gutterWidth) + sourceWidth -= int(native.Width()) + } + source := gui.BoundsWidth(int(gutter.Max)+pad, max(0, sourceWidth)) + + c := codeColumns{ + lineHeight: lineHeight, + pad: pad, + jumpStep: jumpStep, + jump: jump, + asm: asm, + native: native, + gutter: gutter, + source: source, + } + minimumCommentWidth := lineHeight * 4 + + c.sourceTextLeft = int(source.Min) + c.sourceTextWidth = max(int(source.Max)-c.sourceTextLeft, 0) + c.sourceCommentLeft = c.sourceTextLeft + c.sourceTextWidth*70/100 + c.sourceCommentWidth = int(source.Max) - c.sourceCommentLeft + c.sourceCodeWidth = c.sourceCommentLeft - c.sourceTextLeft - pad/2 + if c.sourceCodeWidth < 0 || c.sourceCommentWidth < minimumCommentWidth { + c.sourceCodeWidth = c.sourceTextWidth + c.sourceCommentWidth = 0 + } + + c.goTextLeft = int(asm.Min) + pad/2 + goTextWidth := max(int(asm.Max)-c.goTextLeft, 0) + c.nativeTextLeft = int(native.Min) + c.nativeTextWidth = max(int(native.Max)-c.nativeTextLeft, 0) + c.nativeCommentLeft = c.nativeTextLeft + c.nativeTextWidth*62/100 + c.nativeCommentWidth = int(native.Max) - c.nativeCommentLeft + c.nativeInstructionWidth = c.nativeCommentLeft - c.nativeTextLeft - pad/2 + if c.nativeInstructionWidth < 0 || c.nativeCommentWidth < minimumCommentWidth { + c.nativeInstructionWidth = c.nativeTextWidth + c.nativeCommentWidth = 0 + } + c.commentLeft = max(c.goTextLeft+goTextWidth*62/100, c.goTextLeft) + c.commentWidth = max(int(asm.Max)-c.commentLeft, 0) + c.goInstructionWidth = c.commentLeft - c.goTextLeft - pad/2 + if c.goInstructionWidth < 0 || c.commentWidth < minimumCommentWidth { + c.goInstructionWidth = goTextWidth + c.commentWidth = 0 + } + + return c +} diff --git a/internal/codeview/help.go b/internal/codeview/help.go new file mode 100644 index 0000000..35da3e0 --- /dev/null +++ b/internal/codeview/help.go @@ -0,0 +1,113 @@ +package codeview + +import ( + "image" + "strings" + + "gioui.org/f32" + "gioui.org/font" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/widget/material" + + "loov.dev/lensm/internal/asmhelp" + "loov.dev/lensm/internal/gui" +) + +// layoutHelp draws the instruction help tooltip for the hovered assembly +// line, when help is enabled and the user is not selecting or editing. +func (ui Style) layoutHelp(gtx layout.Context, c codeColumns, hover codeHover) { + commentEditing := ui.CommentEditor != nil && gtx.Focused(ui.CommentEditor) + if !ui.ShowHelp || ui.selecting || commentEditing || !gui.InRange(hover.asmIndex, len(ui.Code.Insts)) { + return + } + inst := ui.Code.Insts[hover.asmIndex] + nativeHovered := ui.ShowNative && c.native.Contains(hover.position.X) + var help asmhelp.Help + var ok bool + if nativeHovered { + help, ok = asmhelp.ForNative(ui.Code.Arch, inst.Mnemonic, inst.NativeText) + } else { + help, ok = asmhelp.ForInstruction(ui.Code.Arch, inst.Mnemonic, inst.Text) + } + if ok { + ui.layoutAssemblyHelp(gtx, help, hover.position) + } +} + +func (ui Style) layoutAssemblyHelp(gtx layout.Context, help asmhelp.Help, position f32.Point) { + maxWidth := gtx.Metric.Dp(460) + if maxWidth > gtx.Constraints.Max.X-16 { + maxWidth = max(0, gtx.Constraints.Max.X-16) + } + if maxWidth == 0 { + return + } + + contentContext := gtx + contentContext.Constraints.Min = image.Point{} + contentContext.Constraints.Max = image.Pt(maxWidth, gtx.Metric.Dp(140)) + macro := op.Record(gtx.Ops) + dims := layout.UniformInset(8).Layout(contentContext, func(gtx layout.Context) layout.Dimensions { + children := []layout.FlexChild{ + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := material.Body1(ui.Theme.Theme, help.Mnemonic+" — "+help.Description) + label.Font.Weight = font.Bold + label.Color = ui.Theme.Colors.Text + label.TextSize = ui.TextHeight * 9 / 10 + return label.Layout(gtx) + }), + } + if help.Explanation != "" { + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := material.Body1(ui.Theme.Theme, help.Explanation) + label.Font.Typeface = "override-monospace,Go,monospace" + label.Color = ui.Syntax.Plain + label.TextSize = ui.TextHeight * 9 / 10 + return layout.Inset{Top: 5}.Layout(gtx, label.Layout) + })) + } + if len(help.Ports) > 0 { + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := material.Body1(ui.Theme.Theme, "ports: "+strings.Join(help.Ports, ", ")) + label.Font.Typeface = "override-monospace,Go,monospace" + label.Color = ui.Syntax.Comment + label.TextSize = ui.TextHeight * 8 / 10 + return layout.Inset{Top: 5}.Layout(gtx, label.Layout) + })) + } + if help.Note != "" { + children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := material.Body1(ui.Theme.Theme, help.Note) + label.Font.Style = font.Italic + label.Color = ui.Syntax.Comment + label.TextSize = ui.TextHeight * 8 / 10 + return layout.Inset{Top: 5}.Layout(gtx, label.Layout) + })) + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) + }) + call := macro.Stop() + + left := int(position.X) + gtx.Metric.Dp(12) + top := int(position.Y) + gtx.Metric.Dp(18) + if left+dims.Size.X > gtx.Constraints.Max.X-4 { + left = gtx.Constraints.Max.X - dims.Size.X - 4 + } + if top+dims.Size.Y > gtx.Constraints.Max.Y-4 { + top = int(position.Y) - dims.Size.Y - gtx.Metric.Dp(8) + } + left = max(4, left) + top = max(4, top) + + stack := op.Offset(image.Pt(left, top)).Push(gtx.Ops) + paint.FillShape(gtx.Ops, ui.Theme.Colors.SecondaryBackground, clip.UniformRRect(image.Rectangle{Max: dims.Size}, 5).Op(gtx.Ops)) + paint.FillShape(gtx.Ops, ui.Theme.Colors.Splitter, clip.Stroke{ + Path: clip.UniformRRect(image.Rectangle{Max: dims.Size}, 5).Path(gtx.Ops), + Width: 1, + }.Op()) + call.Add(gtx.Ops) + stack.Pop() +} diff --git a/internal/codeview/input.go b/internal/codeview/input.go new file mode 100644 index 0000000..d3dc358 --- /dev/null +++ b/internal/codeview/input.go @@ -0,0 +1,296 @@ +package codeview + +import ( + "image" + "math" + "strings" + "time" + + "gioui.org/f32" + "gioui.org/font" + "gioui.org/io/event" + "gioui.org/io/key" + "gioui.org/io/pointer" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/widget" + + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/gui" +) + +// handleInput processes pointer and keyboard events for the frame, +// updating selection and scroll state, and reports whether the release +// was a click (as opposed to a drag). +func (ui Style) handleInput(gtx layout.Context, c codeColumns) (mouseClicked bool) { + lineHeight := c.lineHeight + event.Op(gtx.Ops, ui.UI) + selectionAt := func(position f32.Point) (View, int, bool) { + if c.asm.Contains(position.X) { + relative := position.Y - ui.asm.Offset + if relative < 0 { + return ViewNone, -1, false + } + line := int(relative) / lineHeight + return ViewGoAsm, line, gui.InRange(line, len(ui.Code.Insts)) + } + if ui.ShowNative && c.native.Contains(position.X) { + relative := position.Y - ui.asm.Offset + if relative < 0 { + return ViewNone, -1, false + } + line := int(relative) / lineHeight + return ViewNativeAsm, line, gui.InRange(line, len(ui.Code.Insts)) + } + if c.source.Contains(position.X) { + line := sourceRowAtY(ui.Code, ui.src.Offset, lineHeight, position.Y) + return ViewSource, line, line >= 0 + } + return ViewNone, -1, false + } + // selectionDragLine clamps a drag position to the selection view's + // content, so a fast drag past an edge selects through to the first + // or last line instead of stopping at the last in-range sample. + selectionDragLine := func(view View, position f32.Point) (int, bool) { + var line, count int + switch view { + case ViewGoAsm, ViewNativeAsm: + count = len(ui.Code.Insts) + line = int(position.Y-ui.asm.Offset) / lineHeight + case ViewSource: + count = sourceRowCount(ui.Code) + line = int(position.Y-ui.src.Offset) / lineHeight + default: + return -1, false + } + if count == 0 { + return -1, false + } + return min(max(line, 0), count-1), true + } + for { + ev, ok := gtx.Event(pointer.Filter{ + Target: ui.UI, + Kinds: pointer.Move | pointer.Leave | pointer.Press | pointer.Drag | pointer.Release | pointer.Cancel | pointer.Scroll, + ScrollY: pointer.ScrollRange{ + Min: int(ui.asm.Offset) - lineHeight, + Max: len(ui.Code.Insts)*lineHeight + lineHeight - int(ui.asm.Offset), + }, + }) + if !ok { + break + } + if ev, ok := ev.(pointer.Event); ok { + switch ev.Kind { + case pointer.Move: + ui.mousePosition = ev.Position + case pointer.Leave: + if !ui.selecting { + ui.mousePosition = f32.Pt(-1, -1) + } + case pointer.Press: + ui.mousePosition = ev.Position + if ev.Buttons.Contain(pointer.ButtonPrimary) { + if ui.OnInteract != nil { + ui.OnInteract() + } + view, line, selectable := selectionAt(ev.Position) + if selectable { + ui.Selection.Begin(view, line, ev.Modifiers.Contain(key.ModShift)) + ui.selecting = true + ui.selectionPointer = ev.PointerID + ui.selectionStart = ev.Position + ui.selectionMoved = false + gtx.Execute(pointer.GrabCmd{Tag: ui.UI, ID: ev.PointerID}) + gtx.Execute(key.FocusCmd{Tag: ui.UI}) + } else { + ui.Selection.Clear() + } + } + case pointer.Drag: + ui.mousePosition = ev.Position + if ui.selecting && ev.PointerID == ui.selectionPointer { + if math.Abs(float64(ev.Position.X-ui.selectionStart.X)) > 3 || math.Abs(float64(ev.Position.Y-ui.selectionStart.Y)) > 3 { + ui.selectionMoved = true + } + if line, ok := selectionDragLine(ui.Selection.View, ev.Position); ok { + ui.Selection.Extend(ui.Selection.View, line) + } + } + case pointer.Release: + ui.mousePosition = ev.Position + if ui.selecting && ev.PointerID == ui.selectionPointer { + if line, ok := selectionDragLine(ui.Selection.View, ev.Position); ok { + ui.Selection.Extend(ui.Selection.View, line) + } + mouseClicked = !ui.selectionMoved + ui.selecting = false + } + case pointer.Cancel: + ui.selecting = false + case pointer.Scroll: + ui.mousePosition = ev.Position + switch { + case c.asm.Contains(ev.Position.X): + ui.asm.Offset -= ev.Scroll.Y + case ui.ShowNative && c.native.Contains(ev.Position.X): + ui.asm.Offset -= ev.Scroll.Y + case c.source.Contains(ev.Position.X): + ui.src.Offset -= ev.Scroll.Y + } + } + } + } + selectionFocus := event.Tag(ui.UI) + commentFocused := ui.CommentEditor != nil && gtx.Focused(ui.CommentEditor) + if ui.Selection.Active && !commentFocused { + // A drag can leave keyboard focus on a surrounding widget on macOS. + // An active line selection still owns Cmd/Ctrl+C, Cmd/Ctrl+A, and + // Escape unless the user is editing comment text. (A focused text + // editor elsewhere still wins: it polls earlier in layout order.) + selectionFocus = nil + } + for { + ev, ok := gtx.Event(key.Filter{Focus: selectionFocus, Required: key.ModShortcut, Name: key.Name("C")}) + if !ok { + break + } + keyEvent, ok := ev.(key.Event) + if ok && keyEvent.State == key.Press { + if text := ui.Selection.Text(ui.Code); text != "" && ui.CopyText != nil { + ui.CopyText(gtx, text) + } + } + } + for { + ev, ok := gtx.Event( + key.FocusFilter{Target: ui.UI}, + key.Filter{Focus: selectionFocus, Required: key.ModShortcut, Name: key.Name("A")}, + key.Filter{Focus: selectionFocus, Name: key.NameEscape}, + ) + if !ok { + break + } + keyEvent, ok := ev.(key.Event) + if !ok || keyEvent.State != key.Press { + continue + } + switch keyEvent.Name { + case key.Name("A"): + view := ui.Selection.View + if view == ViewNone { + view = ViewGoAsm + } + lineCount := len(ui.Code.Insts) + if view == ViewSource { + lineCount = sourceRowCount(ui.Code) + } + if lineCount > 0 { + ui.Selection = TextSelection{View: view, Anchor: 0, Head: lineCount - 1, Active: true} + } + case key.NameEscape: + ui.Selection.Clear() + } + } + return mouseClicked +} + +// resolveHover computes the pointer hover state and handles clicks on the +// assembly column: following call targets, selecting a line for comment +// editing, and activating jump animations. +func (ui Style) resolveHover(gtx layout.Context, c codeColumns, mouseClicked bool) codeHover { + lineHeight := c.lineHeight + mousePosition := ui.mousePosition + mouseInAsm := c.asm.Contains(mousePosition.X) || (ui.ShowNative && c.native.Contains(mousePosition.X)) + mouseInSource := c.source.Contains(mousePosition.X) + if mouseInAsm || mouseInSource { + pointer.CursorText.Add(gtx.Ops) + } + highlightAsmIndex := -1 + if relative := mousePosition.Y - ui.asm.Offset; mouseInAsm && relative >= 0 { + highlightAsmIndex = int(relative) / lineHeight + } + + if gui.InRange(highlightAsmIndex, len(ui.Code.Insts)) { + activateClicked := mouseClicked && ui.SelectedAsm == highlightAsmIndex + ix := &ui.Code.Insts[highlightAsmIndex] + callTargetHovered := ui.TryOpen != nil && + ix.Call != "" && + c.asm.Contains(mousePosition.X) && + mousePosition.X <= float32(c.goTextLeft+c.goInstructionWidth) && + ui.callTargetHit(gtx, *ix, c.goTextLeft, mousePosition.X) + if callTargetHovered { + pointer.CursorPointer.Add(gtx.Ops) + if mouseClicked { + ui.SelectedAsm = highlightAsmIndex + ui.SelectedView = ViewGoAsm + ui.SelectedFile = "" + ui.SelectedLine = 0 + ui.TryOpen(gtx, ix.Call) + } + } else if mouseClicked && ix.Text != "" { + // Spacer rows (empty synthetic instructions) have no inline + // editor; focusing it would swallow subsequent typing. + ui.SelectedAsm = highlightAsmIndex + ui.SelectedFile = "" + ui.SelectedLine = 0 + if ui.ShowNative && c.native.Contains(mousePosition.X) { + ui.SelectedView = ViewNativeAsm + } else { + ui.SelectedView = ViewGoAsm + } + if ui.CommentEditor != nil { + gtx.Execute(key.FocusCmd{Tag: ui.CommentEditor}) + } + } + if ix.Call == "" && ix.RefOffset != 0 { + pointer.CursorPointer.Add(gtx.Ops) + if activateClicked { + // TODO: smooth scroll + // highlightAsmIndex -= ix.RefOffset + ui.asm.Anim.Start(gtx, ui.asm.Offset, ui.asm.Offset-float32(ix.RefOffset*lineHeight), 150*time.Millisecond) + } + } + } + if !gui.InRange(ui.SelectedAsm, len(ui.Code.Insts)) { + ui.SelectedAsm = -1 + } + + return codeHover{ + position: mousePosition, + inAsm: mouseInAsm, + inSource: mouseInSource, + asmIndex: highlightAsmIndex, + } +} + +func (ui Style) callTargetHit(gtx layout.Context, inst disasm.Inst, left int, x float32) bool { + if inst.Call == "" { + return false + } + start := strings.Index(inst.Text, inst.Call) + if start < 0 { + return false + } + // A hovered call line is drawn bold and italic; measure with the + // same style, otherwise the hitbox drifts from the visible text + // whenever the fallback font is proportional. + f := font.Font{Typeface: "override-monospace,Go,monospace", Weight: font.Black, Style: font.Italic} + end := start + len(inst.Call) + targetLeft := left + ui.measureAsmTextWidth(gtx, f, inst.Text[:start]) + targetRight := left + ui.measureAsmTextWidth(gtx, f, inst.Text[:end]) + return float32(targetLeft) <= x && x <= float32(targetRight) +} + +func (ui Style) measureAsmTextWidth(gtx layout.Context, f font.Font, text string) int { + if text == "" { + return 0 + } + gtx.Constraints.Min = image.Point{} + gtx.Constraints.Max = image.Pt(gui.MaxLineWidth, gui.MaxLineWidth) + + macro := op.Record(gtx.Ops) + dims := widget.Label{MaxLines: 1}.Layout(gtx, ui.Theme.Shaper, f, ui.TextHeight, text, op.CallOp{}) + _ = macro.Stop() + return dims.Size.X +} diff --git a/internal/codeview/selection.go b/internal/codeview/selection.go new file mode 100644 index 0000000..ccd0402 --- /dev/null +++ b/internal/codeview/selection.go @@ -0,0 +1,188 @@ +package codeview + +import ( + "strings" + + "loov.dev/lensm/internal/comments" + "loov.dev/lensm/internal/disasm" +) + +type View uint8 + +const ( + ViewNone View = iota + ViewGoAsm + ViewNativeAsm + ViewSource +) + +// CommentView maps a pane to its comment-store view. ok is false for +// CodeViewNone, which has no comments. +func (v View) CommentView() (view comments.View, ok bool) { + switch v { + case ViewGoAsm: + return comments.ViewGoAsm, true + case ViewNativeAsm: + return comments.ViewNativeAsm, true + case ViewSource: + return comments.ViewSource, true + default: + return "", false + } +} + +type TextSelection struct { + View View + Anchor int + Head int + Active bool +} + +func (s *TextSelection) Clear() { + *s = TextSelection{} +} + +func (s *TextSelection) Begin(view View, line int, extend bool) { + if view == ViewNone || line < 0 { + s.Clear() + return + } + if !extend || !s.Active || s.View != view { + s.View = view + s.Anchor = line + } + s.Head = line + s.Active = true +} + +func (s *TextSelection) Extend(view View, line int) { + if !s.Active || s.View != view || line < 0 { + return + } + s.Head = line +} + +func (s TextSelection) Range() (from, to int, ok bool) { + if !s.Active { + return 0, 0, false + } + from, to = s.Anchor, s.Head + if from > to { + from, to = to, from + } + return from, to, true +} + +func (s TextSelection) Contains(view View, line int) bool { + if s.View != view { + return false + } + from, to, ok := s.Range() + return ok && from <= line && line <= to +} + +type sourceTextRow struct { + Text string +} + +func sourceTextRows(code *disasm.Code) []sourceTextRow { + if code == nil { + return nil + } + var rows []sourceTextRow + for sourceIndex, source := range code.Source { + if sourceIndex > 0 { + rows = append(rows, sourceTextRow{}) + } + rows = append(rows, sourceTextRow{Text: "// " + source.File}) + for blockIndex, block := range source.Blocks { + if blockIndex > 0 { + rows = append(rows, sourceTextRow{}) + } + for _, line := range block.Lines { + rows = append(rows, sourceTextRow{Text: line}) + } + } + } + return rows +} + +// sourceRowCount mirrors the rows produced by sourceTextRows without +// building them; it runs on every pointer event. +func sourceRowCount(code *disasm.Code) int { + if code == nil { + return 0 + } + count := 0 + for sourceIndex, source := range code.Source { + if sourceIndex > 0 { + count++ + } + count++ + for blockIndex, block := range source.Blocks { + if blockIndex > 0 { + count++ + } + count += len(block.Lines) + } + } + return count +} + +func sourceRowAtY(code *disasm.Code, scroll float32, lineHeight int, y float32) int { + if code == nil || lineHeight <= 0 { + return -1 + } + relative := y - scroll + if relative < 0 { + return -1 + } + row := int(relative / float32(lineHeight)) + if row < 0 || row >= sourceRowCount(code) { + return -1 + } + return row +} + +func (s TextSelection) Text(code *disasm.Code) string { + if code == nil { + return "" + } + from, to, ok := s.Range() + if !ok { + return "" + } + + var lines []string + switch s.View { + case ViewGoAsm, ViewNativeAsm: + if from < 0 { + from = 0 + } + if to >= len(code.Insts) { + to = len(code.Insts) - 1 + } + for i := from; i <= to; i++ { + text := code.Insts[i].Text + if s.View == ViewNativeAsm { + text = strings.ToUpper(code.Insts[i].NativeText) + } + lines = append(lines, text) + } + case ViewSource: + rows := sourceTextRows(code) + if from < 0 { + from = 0 + } + if to >= len(rows) { + to = len(rows) - 1 + } + for i := from; i <= to; i++ { + lines = append(lines, rows[i].Text) + } + } + if len(lines) == 0 { + return "" + } + return strings.Join(lines, "\n") + "\n" +} diff --git a/internal/codeview/selection_test.go b/internal/codeview/selection_test.go new file mode 100644 index 0000000..a612845 --- /dev/null +++ b/internal/codeview/selection_test.go @@ -0,0 +1,44 @@ +package codeview + +import ( + "testing" + + "loov.dev/lensm/internal/disasm" +) + +func TestTextSelectionAssembly(t *testing.T) { + code := &disasm.Code{Insts: []disasm.Inst{ + {Text: "MOV (R2), R1", NativeText: "mov (%r2), %r1"}, + {Text: "ADDQ $1, R1", NativeText: "addq $1, %r1"}, + {Text: "RET", NativeText: "ret"}, + }} + selection := TextSelection{View: ViewGoAsm, Anchor: 1, Head: 0, Active: true} + if got, want := selection.Text(code), "MOV (R2), R1\nADDQ $1, R1\n"; got != want { + t.Fatalf("Text() = %q, want %q", got, want) + } + selection.View = ViewNativeAsm + if got, want := selection.Text(code), "MOV (%R2), %R1\nADDQ $1, %R1\n"; got != want { + t.Fatalf("native Text() = %q, want %q", got, want) + } +} + +func TestTextSelectionSource(t *testing.T) { + code := &disasm.Code{Source: []disasm.Source{{ + File: "main.go", + Blocks: []disasm.SourceBlock{{Lines: []string{"func main() {", "}"}}}, + }}} + selection := TextSelection{View: ViewSource, Anchor: 0, Head: 2, Active: true} + if got, want := selection.Text(code), "// main.go\nfunc main() {\n}\n"; got != want { + t.Fatalf("Text() = %q, want %q", got, want) + } +} + +func TestSourceRowAtYRejectsSpaceAboveContent(t *testing.T) { + code := &disasm.Code{Source: []disasm.Source{{File: "main.go"}}} + if got := sourceRowAtY(code, 10, 20, 9); got != -1 { + t.Fatalf("sourceRowAtY() = %d, want -1", got) + } + if got := sourceRowAtY(code, 10, 20, 10); got != 0 { + t.Fatalf("sourceRowAtY() = %d, want 0", got) + } +} diff --git a/internal/comments/comments.go b/internal/comments/comments.go new file mode 100644 index 0000000..8467b61 --- /dev/null +++ b/internal/comments/comments.go @@ -0,0 +1,477 @@ +package comments + +import ( + "cmp" + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + "strings" + "sync" + "time" + + "loov.dev/lensm/internal/atomicfile" +) + +const commentsFileVersion = 1 + +type View string + +const ( + ViewSource View = "source" + ViewGoAsm View = "go_asm" + ViewNativeAsm View = "native_asm" +) + +type Coord struct { + Binary string `json:"binary,omitempty"` + Function string `json:"function"` + View View `json:"view"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` + PC uint64 `json:"pc,omitempty"` + PCHex string `json:"pc_hex,omitempty"` +} + +type Record struct { + Coord + Text string `json:"text"` + UpdatedAt time.Time `json:"updated_at"` +} + +type commentsDiskFile struct { + Version int `json:"version"` + Binary string `json:"binary,omitempty"` + Comments []json.RawMessage `json:"comments"` +} + +type Store struct { + mu sync.RWMutex + path string + binary string + records map[string]Record + // touched marks keys mutated since the last sync with the file; + // saves merge the on-disk state for all other keys, so another lensm + // process writing the same sidecar loses only conflicting edits to + // the same record, not its whole set of comments. + touched map[string]bool + // dirty marks in-memory changes not yet written by Flush. + dirty bool + // preserved holds records this version doesn't understand (e.g. views + // added by a newer lensm), kept verbatim so saving doesn't destroy them. + preserved []json.RawMessage +} + +func DefaultPath(binaryPath string) string { + if binaryPath == "" { + return "" + } + return binaryPath + ".lensm-comments.json" +} + +func Open(path, binaryPath string) (*Store, error) { + binaryPath = CleanPath(binaryPath) + store := &Store{ + path: path, + binary: binaryPath, + records: map[string]Record{}, + touched: map[string]bool{}, + } + if path == "" { + return store, nil + } + if err := store.load(); err != nil { + return nil, err + } + return store, nil +} + +func (store *Store) Path() string { + if store == nil { + return "" + } + return store.path +} + +func (store *Store) Get(coord Coord) string { + if store == nil { + return "" + } + store.mu.RLock() + defer store.mu.RUnlock() + rec, ok := store.records[store.key(coord)] + if !ok { + return "" + } + return rec.Text +} + +func (store *Store) Set(coord Coord, text string) error { + return store.set(coord, text, true) +} + +// SetBuffered updates the comment in memory and defers the disk write +// until Flush, so typing in an editor doesn't rewrite the sidecar on +// every keystroke. +func (store *Store) SetBuffered(coord Coord, text string) error { + return store.set(coord, text, false) +} + +func (store *Store) set(coord Coord, text string, persist bool) error { + if store == nil { + return errors.New("comment store is not initialized") + } + text = strings.TrimSpace(text) + if err := coord.validate(); err != nil { + return err + } + + store.mu.Lock() + defer store.mu.Unlock() + key := store.key(coord) + existing, exists := store.records[key] + if text == "" { + // Even when the record isn't in memory it may exist on disk, + // written by another process sharing the sidecar; fall through so + // the deletion is recorded and the next save drops it instead of + // re-adopting it. + delete(store.records, key) + } else { + coord = store.Normalize(coord) + if exists && existing.Text == text { + return nil + } + store.records[key] = Record{ + Coord: coord, + Text: text, + UpdatedAt: time.Now().UTC(), + } + } + wasTouched := store.touched[key] + store.touched[key] = true + wasDirty := store.dirty + store.dirty = true + if !persist { + return nil + } + if err := store.saveLocked(); err != nil { + // Roll back so the in-memory store keeps matching the file. + if exists { + store.records[key] = existing + } else { + delete(store.records, key) + } + if !wasTouched { + delete(store.touched, key) + } + store.dirty = wasDirty + return err + } + store.dirty = false + return nil +} + +// Flush writes buffered changes to disk. +func (store *Store) Flush() error { + if store == nil { + return nil + } + store.mu.Lock() + defer store.mu.Unlock() + if !store.dirty { + return nil + } + if err := store.saveLocked(); err != nil { + return err + } + store.dirty = false + return nil +} + +func (store *Store) All() []Record { + if store == nil { + return nil + } + store.mu.RLock() + defer store.mu.RUnlock() + out := make([]Record, 0, len(store.records)) + for _, rec := range store.records { + out = append(out, rec) + } + sortCommentRecords(out) + return out +} + +func (store *Store) Filter(function string, view View) []Record { + if store == nil { + return nil + } + store.mu.RLock() + defer store.mu.RUnlock() + var out []Record + for _, rec := range store.records { + if function != "" && rec.Function != function { + continue + } + if view != "" && rec.View != view { + continue + } + out = append(out, rec) + } + sortCommentRecords(out) + return out +} + +func (store *Store) ForSource(function, file string, line int) string { + return store.Get(Coord{ + Function: function, + View: ViewSource, + File: file, + Line: line, + }) +} + +func (store *Store) ForAsm(function string, view View, pc uint64) string { + return store.Get(Coord{ + Function: function, + View: view, + PC: pc, + }) +} + +func (store *Store) SetSource(function, file string, line int, text string) error { + return store.Set(Coord{ + Function: function, + View: ViewSource, + File: file, + Line: line, + }, text) +} + +func (store *Store) SetAsm(function string, view View, pc uint64, text string) error { + return store.Set(Coord{ + Function: function, + View: view, + PC: pc, + }, text) +} + +func (store *Store) load() error { + data, err := os.ReadFile(store.path) + if errors.Is(err, os.ErrNotExist) { + return nil + } + if err != nil { + return err + } + + var disk commentsDiskFile + if err := json.Unmarshal(data, &disk); err != nil { + return fmt.Errorf("load comments: %w", err) + } + if disk.Version != 0 && disk.Version != commentsFileVersion { + return fmt.Errorf("unsupported comments file version %d", disk.Version) + } + for _, raw := range disk.Comments { + var rec Record + if err := json.Unmarshal(raw, &rec); err != nil { + store.preserved = append(store.preserved, raw) + continue + } + if rec.Binary == "" { + rec.Binary = cmp.Or(disk.Binary, store.binary) + } + rec.PCHex = commentPCHex(rec.Coord) + if err := rec.Coord.validate(); err != nil { + store.preserved = append(store.preserved, raw) + continue + } + if rec.Text == "" { + continue + } + store.records[store.key(rec.Coord)] = rec + } + return nil +} + +func (store *Store) saveLocked() error { + if store.path == "" { + return nil + } + store.mergeExternalLocked() + records := make([]Record, 0, len(store.records)) + for _, rec := range store.records { + records = append(records, rec) + } + sortCommentRecords(records) + + // Don't create a file just to record that nothing exists, e.g. after + // deleting a comment that was never saved. + if len(records) == 0 && len(store.preserved) == 0 { + if _, err := os.Stat(store.path); errors.Is(err, os.ErrNotExist) { + clear(store.touched) + return nil + } + } + + comments := make([]json.RawMessage, 0, len(records)+len(store.preserved)) + for _, rec := range records { + raw, err := json.Marshal(rec) + if err != nil { + return err + } + comments = append(comments, raw) + } + comments = append(comments, store.preserved...) + + data, err := json.MarshalIndent(commentsDiskFile{ + Version: commentsFileVersion, + Binary: store.binary, + Comments: comments, + }, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + + if err := os.MkdirAll(filepath.Dir(store.path), 0o755); err != nil { + return err + } + if err := atomicfile.Write(store.path, data, 0o644); err != nil { + return err + } + // The in-memory state now matches the file again. + clear(store.touched) + return nil +} + +// mergeExternalLocked folds records another lensm process wrote to the +// sidecar into the in-memory store before a full-file rewrite. Keys +// mutated locally since the last sync win; everything else — additions, +// edits, and deletions from the other process — is adopted, so two +// processes sharing a sidecar lose at most conflicting edits to the +// same record instead of each other's whole comment sets. +func (store *Store) mergeExternalLocked() { + data, err := os.ReadFile(store.path) + if err != nil { + return // nothing on disk to merge + } + var disk commentsDiskFile + if err := json.Unmarshal(data, &disk); err != nil { + return + } + if disk.Version != 0 && disk.Version != commentsFileVersion { + return + } + + merged := make(map[string]Record, len(store.records)) + for key, rec := range store.records { + if store.touched[key] { + merged[key] = rec + } + } + store.preserved = nil + for _, raw := range disk.Comments { + var rec Record + if err := json.Unmarshal(raw, &rec); err != nil { + store.preserved = append(store.preserved, raw) + continue + } + if rec.Binary == "" { + rec.Binary = cmp.Or(disk.Binary, store.binary) + } + rec.PCHex = commentPCHex(rec.Coord) + if err := rec.Coord.validate(); err != nil { + store.preserved = append(store.preserved, raw) + continue + } + if rec.Text == "" { + continue + } + key := store.key(rec.Coord) + if store.touched[key] { + continue + } + merged[key] = rec + } + store.records = merged +} + +func (store *Store) Normalize(coord Coord) Coord { + coord.Binary = cmp.Or(coord.Binary, store.binary) + coord.Binary = CleanPath(coord.Binary) + coord.PCHex = commentPCHex(coord) + return coord +} + +func (store *Store) key(coord Coord) string { + coord = store.Normalize(coord) + switch coord.View { + case ViewSource: + return fmt.Sprintf("%s\x00%s\x00%s\x00%d", coord.Function, coord.View, CleanPath(coord.File), coord.Line) + case ViewGoAsm, ViewNativeAsm: + return fmt.Sprintf("%s\x00%s\x00%x", coord.Function, coord.View, coord.PC) + default: + return fmt.Sprintf("%s\x00%s\x00%s\x00%d\x00%x", coord.Function, coord.View, CleanPath(coord.File), coord.Line, coord.PC) + } +} + +func (coord Coord) validate() error { + if coord.Function == "" { + return errors.New("function is required") + } + switch coord.View { + case ViewSource: + if coord.File == "" { + return errors.New("file is required for source comments") + } + if coord.Line <= 0 { + return errors.New("line must be positive for source comments") + } + case ViewGoAsm, ViewNativeAsm: + default: + return fmt.Errorf("unsupported comment view %q", coord.View) + } + return nil +} + +func sortCommentRecords(records []Record) { + slices.SortFunc(records, func(a, b Record) int { + if c := cmp.Compare(a.Function, b.Function); c != 0 { + return c + } + if c := cmp.Compare(a.View, b.View); c != 0 { + return c + } + if c := cmp.Compare(a.File, b.File); c != 0 { + return c + } + if c := cmp.Compare(a.Line, b.Line); c != 0 { + return c + } + return cmp.Compare(a.PC, b.PC) + }) +} + +func CleanPath(path string) string { + if path == "" { + return "" + } + if abs, err := filepath.Abs(path); err == nil { + return filepath.Clean(abs) + } + return filepath.Clean(path) +} + +func FormatPC(pc uint64) string { + return fmt.Sprintf("0x%x", pc) +} + +func commentPCHex(coord Coord) string { + if coord.View == ViewGoAsm || coord.View == ViewNativeAsm || coord.PC != 0 { + return FormatPC(coord.PC) + } + return "" +} diff --git a/internal/comments/comments_test.go b/internal/comments/comments_test.go new file mode 100644 index 0000000..fb00aa0 --- /dev/null +++ b/internal/comments/comments_test.go @@ -0,0 +1,246 @@ +package comments + +import ( + "bytes" + "os" + "path/filepath" + "testing" + "time" +) + +func TestCommentStoreRoundTrip(t *testing.T) { + path := filepath.Join(t.TempDir(), "comments.json") + store, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + + if err := store.SetSource("main.add", "/tmp/main.go", 12, "source note"); err != nil { + t.Fatal(err) + } + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, "go asm note"); err != nil { + t.Fatal(err) + } + if err := store.SetAsm("main.add", ViewNativeAsm, 0x1000, "native asm note"); err != nil { + t.Fatal(err) + } + + reloaded, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + if got := reloaded.ForSource("main.add", "/tmp/main.go", 12); got != "source note" { + t.Fatalf("source comment = %q", got) + } + if got := reloaded.ForAsm("main.add", ViewGoAsm, 0x1000); got != "go asm note" { + t.Fatalf("go asm comment = %q", got) + } + if got := reloaded.ForAsm("main.add", ViewNativeAsm, 0x1000); got != "native asm note" { + t.Fatalf("native asm comment = %q", got) + } +} + +func TestCommentStoreDeletesEmptyComment(t *testing.T) { + path := filepath.Join(t.TempDir(), "comments.json") + store, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, "note"); err != nil { + t.Fatal(err) + } + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, " "); err != nil { + t.Fatal(err) + } + + reloaded, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + if got := reloaded.ForAsm("main.add", ViewGoAsm, 0x1000); got != "" { + t.Fatalf("deleted comment = %q", got) + } + if got := len(reloaded.All()); got != 0 { + t.Fatalf("comment count = %d", got) + } +} + +func TestCommentStoreSkipsUnchangedCommentWrite(t *testing.T) { + path := filepath.Join(t.TempDir(), "comments.json") + store, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, "note"); err != nil { + t.Fatal(err) + } + + before, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + records := store.All() + if len(records) != 1 { + t.Fatalf("comment count = %d", len(records)) + } + updatedAt := records[0].UpdatedAt + + time.Sleep(time.Millisecond) + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, "note"); err != nil { + t.Fatal(err) + } + after, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(before, after) { + t.Fatal("unchanged comment rewrote comments file") + } + records = store.All() + if len(records) != 1 { + t.Fatalf("comment count = %d", len(records)) + } + if !records[0].UpdatedAt.Equal(updatedAt) { + t.Fatalf("updated_at changed from %s to %s", updatedAt, records[0].UpdatedAt) + } +} + +func TestCommentStorePreservesUnknownRecords(t *testing.T) { + path := filepath.Join(t.TempDir(), "comments.json") + seed := `{ + "version": 1, + "comments": [ + {"function": "main.add", "view": "hologram", "pc": 16, "text": "from the future"}, + {"function": "main.add", "view": "go_asm", "pc_hex": "0x1000", "pc": 4096, "text": "known"} + ] +}` + if err := os.WriteFile(path, []byte(seed), 0o644); err != nil { + t.Fatal(err) + } + + store, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + // Trigger a full rewrite of the file. + if err := store.SetAsm("main.add", ViewGoAsm, 0x2000, "new note"); err != nil { + t.Fatal(err) + } + + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if !bytes.Contains(data, []byte("hologram")) { + t.Fatalf("unknown-view record dropped on save:\n%s", data) + } + if !bytes.Contains(data, []byte("known")) || !bytes.Contains(data, []byte("new note")) { + t.Fatalf("known records missing after save:\n%s", data) + } +} + +func TestCommentStoreSaveKeepsFileShareable(t *testing.T) { + path := filepath.Join(t.TempDir(), "comments.json") + store, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, "note"); err != nil { + t.Fatal(err) + } + info, err := os.Stat(path) + if err != nil { + t.Fatal(err) + } + if got := info.Mode().Perm(); got != 0o644 { + t.Fatalf("comments file mode = %o, want 644", got) + } +} + +func TestCommentStoreSkipsMissingCommentDelete(t *testing.T) { + path := filepath.Join(t.TempDir(), "comments.json") + store, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, ""); err != nil { + t.Fatal(err) + } + if _, err := os.Stat(path); !os.IsNotExist(err) { + t.Fatalf("comments file exists after deleting missing comment: %v", err) + } +} + +func TestCommentStoreMergesConcurrentProcessWrites(t *testing.T) { + path := filepath.Join(t.TempDir(), "comments.json") + first, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + second, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + + // Each store simulates a separate lensm process holding a stale + // full-file snapshot of the same sidecar. + if err := first.SetAsm("main.add", ViewGoAsm, 0x1000, "from first"); err != nil { + t.Fatal(err) + } + if err := second.SetAsm("main.add", ViewGoAsm, 0x2000, "from second"); err != nil { + t.Fatal(err) + } + + reloaded, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + if got := reloaded.ForAsm("main.add", ViewGoAsm, 0x1000); got != "from first" { + t.Fatalf("first process's comment = %q, want it to survive the second save", got) + } + if got := reloaded.ForAsm("main.add", ViewGoAsm, 0x2000); got != "from second" { + t.Fatalf("second process's comment = %q", got) + } + + // A deletion must not be resurrected by the other process's stale copy. + if err := first.SetAsm("main.add", ViewGoAsm, 0x2000, ""); err != nil { + t.Fatal(err) + } + reloaded, err = Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + if got := reloaded.ForAsm("main.add", ViewGoAsm, 0x2000); got != "" { + t.Fatalf("deleted comment resurrected: %q", got) + } +} + +func TestCommentStoreRollsBackFailedSave(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "comments.json") + store, err := Open(path, "/tmp/app") + if err != nil { + t.Fatal(err) + } + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, "old"); err != nil { + t.Fatal(err) + } + + store.path = filepath.Join(dir, "missing", "comments.json") + if err := os.WriteFile(filepath.Join(dir, "missing"), []byte("not a directory"), 0o644); err != nil { + t.Fatal(err) + } + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, "new"); err == nil { + t.Fatal("updating comment unexpectedly succeeded") + } + if got := store.ForAsm("main.add", ViewGoAsm, 0x1000); got != "old" { + t.Fatalf("comment after failed update = %q, want old", got) + } + + if err := store.SetAsm("main.add", ViewGoAsm, 0x1000, ""); err == nil { + t.Fatal("deleting comment unexpectedly succeeded") + } + if got := store.ForAsm("main.add", ViewGoAsm, 0x1000); got != "old" { + t.Fatalf("comment after failed delete = %q, want old", got) + } +} diff --git a/internal/disasm/code.go b/internal/disasm/code.go new file mode 100644 index 0000000..0d42c1b --- /dev/null +++ b/internal/disasm/code.go @@ -0,0 +1,70 @@ +package disasm + +// Code combines the disassembly and the source code mapping. +type Code struct { + // Name is the name of the code block, e.g. function or method name. + Name string + // File is where the code is located. + File string + // Arch is the GOARCH of the disassembled binary. + Arch string + + // Insts is the slice of a all instructions in the code. + Insts []Inst + // MaxJump is the maximum layers of jumps for the codeblock. + // This is used to determine how much padding is created for + // the jump lines. + MaxJump int + + // Source is the slice of a codeblocks that were used to create the instructions. + Source []Source +} + +// Inst represents a single instruction. +type Inst struct { + // PC is the program counter, usually offset in the binary. + PC uint64 + // Text is the textual representation of this instruction. + Text string + // NativeText is the native assembler syntax for this instruction. + NativeText string + // Mnemonic is the canonical decoder mnemonic (e.g. "LD1" where Text spells + // it "VLD1"), used for reference lookups. Empty for undecodable bytes. + Mnemonic string + // File is the location where this instruction was compiled from. + File string + // Line is the line in the file where this instruction was compiled from. + Line int + + // RefPC is a reference to another program counter, e.g. a call. + RefPC uint64 + // RefOffset is a reference to a relative jump. + RefOffset int + // RefStack is the depth that the jump line should be drawn at. + RefStack int + + // Call is a named target that should be present in Funcs. + // This is used to make the instruction clickable and follow to the + // called target. + Call string +} + +// Source represents code from a single file. +type Source struct { + // File is the file name for the source code. + File string + // Blocks is a slice of blocks that were used for compiling the instructions. + Blocks []SourceBlock +} + +// SourceBlock represents a single sequential codeblock that references the instructions. +type SourceBlock struct { + // LineRange is the range of lines that it references from the file. + LineRange + // Lines are textual representation of the source starting from `LineRange.From`. + Lines []string + // Related contains a set of ranges in the instructions. + // e.g. for source code Lines[5] there will be drawn relation shapes to each + // instructions `for _, r := range Related[5] { draw(Insts[r.From:r.To]) }` + Related [][]LineRange +} diff --git a/internal/disasm/file.go b/internal/disasm/file.go new file mode 100644 index 0000000..6338f19 --- /dev/null +++ b/internal/disasm/file.go @@ -0,0 +1,24 @@ +package disasm + +// File represents an object file, a module or anything that contains functions. +type File interface { + // Close closes the underlying data. + Close() error + // Funcs enumerates all the visualizable code blocks. + Funcs() []Func +} + +// Func represents a function or method that can be independently rendered. +type Func interface { + // Name is the name of the func. + Name() string + // Load loads the source code and disassembles it. + Load(opt Options) (*Code, error) +} + +// Options defines configuration for loading the func. +type Options struct { + // Context is the number of lines that should be additionally included for context. + // This can often contain function documentation. + Context int +} diff --git a/internal/disasm/linerange.go b/internal/disasm/linerange.go new file mode 100644 index 0000000..c76b0b5 --- /dev/null +++ b/internal/disasm/linerange.go @@ -0,0 +1,14 @@ +package disasm + +// LineRange represents a list of lines. +type LineRange struct{ From, To int } + +// LineRangesContain checks whether line a or line b is contained in the ranges. +func LineRangesContain(ranges []LineRange, a, b int) bool { + for _, r := range ranges { + if (r.From <= a && a < r.To) || (r.From <= b && b < r.To) { + return true + } + } + return false +} diff --git a/internal/disasm/lineset.go b/internal/disasm/lineset.go new file mode 100644 index 0000000..1480cf1 --- /dev/null +++ b/internal/disasm/lineset.go @@ -0,0 +1,62 @@ +package disasm + +import "slices" + +// LineSet represents a set of needed lines. +type LineSet struct { + list []int +} + +// Add adds line to the needed set. +func (rs *LineSet) Add(line int) { + if at, found := slices.BinarySearch(rs.list, line); !found { + rs.list = slices.Insert(rs.list, at, line) + } +} + +// Ranges converts line set to line ranges and adds context for extra information. +func (rs *LineSet) Ranges(context int) []LineRange { + if len(rs.list) == 0 { + return nil + } + + var all []LineRange + + current := LineRange{From: rs.list[0] - context, To: rs.list[0] + context + 1} + if current.From < 1 { + current.From = 1 + } + for _, line := range rs.list { + if line-context <= current.To { + current.To = line + context + 1 + } else { + all = append(all, current) + current = LineRange{From: line - context, To: line + context + 1} + } + } + all = append(all, current) + + return all +} + +// RangesZero returns a ranges without expanding by context. +func (rs *LineSet) RangesZero() []LineRange { + if len(rs.list) == 0 { + return nil + } + + var all []LineRange + + current := LineRange{From: rs.list[0], To: rs.list[0] + 1} + for _, line := range rs.list { + if line <= current.To { + current.To = line + 1 + } else { + all = append(all, current) + current = LineRange{From: line, To: line + 1} + } + } + all = append(all, current) + + return all +} diff --git a/internal/f32color/color.go b/internal/f32color/color.go new file mode 100644 index 0000000..59de7a5 --- /dev/null +++ b/internal/f32color/color.go @@ -0,0 +1,127 @@ +package f32color + +import ( + "image/color" + "math" +) + +var ( + White = color.NRGBA{0xFF, 0xFF, 0xFF, 0xFF} + Black = color.NRGBA{0x00, 0x00, 0x00, 0xFF} + Red = color.NRGBA{0xFF, 0x00, 0x00, 0xFF} + Green = color.NRGBA{0x00, 0xFF, 0x00, 0xFF} + Blue = color.NRGBA{0x00, 0x00, 0xFF, 0xFF} + Yellow = color.NRGBA{0xFF, 0xFF, 0x00, 0xFF} + + Transparent = color.NRGBA{0xFF, 0xFF, 0xFF, 0x00} +) + +func NRGBAHex(hex uint32) color.NRGBA { + return color.NRGBA{ + R: uint8(hex >> 24), + G: uint8(hex >> 16), + B: uint8(hex >> 8), + A: uint8(hex >> 0), + } +} + +// Gray8 returns the corresponding gray value. +func Gray8(v byte) color.NRGBA { + return color.NRGBA{R: v, G: v, B: v, A: 0xFF} +} + +// RGB returns color based on RGB in range 0..1 +func RGB(r, g, b float32) color.NRGBA { + return color.NRGBA{R: sat8(r), G: sat8(g), B: sat8(b), A: 0xFF} +} + +// RGBA returns color based on RGBA in range 0..1 +func RGBA(r, g, b, a float32) color.NRGBA { + return color.NRGBA{R: sat8(r), G: sat8(g), B: sat8(b), A: sat8(a)} +} + +// HSLA returns color based on HSLA in range 0..1 +func HSLA(h, s, l, a float32) color.NRGBA { return RGBA(hsla(h, s, l, a)) } + +// HSL returns color based on HSL in range 0..1 +func HSL(h, s, l float32) color.NRGBA { return HSLA(h, s, l, 1) } + +// RGBAFloat returns RGBA scaled to 0..1 +func RGBAFloat(c color.NRGBA) (r, g, b, a float32) { + return float32(c.R) / 0xFF, float32(c.G) / 0xFF, float32(c.B) / 0xFF, float32(c.A) / 0xFF +} + +// Lerp linearly interpolates each RGBA component separately +func RGBALerp(a, b color.NRGBA, p float32) color.NRGBA { + ar, ag, ab, aa := RGBAFloat(a) + br, bg, bb, ba := RGBAFloat(b) + return RGBA( + lerpClamp(ar, br, p), + lerpClamp(ag, bg, p), + lerpClamp(ab, bb, p), + lerpClamp(aa, ba, p), + ) +} + +func hue(v1, v2, h float32) float32 { + if h < 0 { + h += 1 + } + if h > 1 { + h -= 1 + } + if 6*h < 1 { + return v1 + (v2-v1)*6*h + } else if 2*h < 1 { + return v2 + } else if 3*h < 2 { + return v1 + (v2-v1)*(2.0/3.0-h)*6 + } + + return v1 +} + +func hsla(h, s, l, a float32) (r, g, b, ra float32) { + if s == 0 { + return l, l, l, a + } + + h = mod32(h, 1) + + var v2 float32 + if l < 0.5 { + v2 = l * (1 + s) + } else { + v2 = (l + s) - s*l + } + + v1 := 2*l - v2 + r = hue(v1, v2, h+1.0/3.0) + g = hue(v1, v2, h) + b = hue(v1, v2, h-1.0/3.0) + ra = a + + return +} + +// sat8 converts 0..1 float to 0..255 uint8 +func sat8(v float32) uint8 { + v *= 255.0 + if v >= 255 { + return 255 + } else if v <= 0 { + return 0 + } + return uint8(v) +} + +func lerpClamp(p, min, max float32) float32 { + if p < 0 { + return min + } else if p > 1 { + return max + } + return min + (max-min)*p +} + +func mod32(x, y float32) float32 { return float32(math.Mod(float64(x), float64(y))) } diff --git a/internal/go/LICENSE b/internal/go/LICENSE new file mode 100644 index 0000000..ea5ea89 --- /dev/null +++ b/internal/go/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009 The Go Authors. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * 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. + * Neither the name of Google Inc. 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 +OWNER 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. \ No newline at end of file diff --git a/internal/go/expose.go_ b/internal/go/expose.go_ new file mode 100644 index 0000000..3d0a4a5 --- /dev/null +++ b/internal/go/expose.go_ @@ -0,0 +1,49 @@ +package disasm + +import ( + "strings" + + "loov.dev/lensm/internal/go/src/objfile" +) + +func (d *Disasm) Syms() []objfile.Sym { return d.syms } +func (d *Disasm) TextStart() uint64 { return d.textStart } +func (d *Disasm) TextEnd() uint64 { return d.textEnd } +func (d *Disasm) PCLN() objfile.Liner { return d.pcln } +func (d *Disasm) GOARCH() string { return d.goarch } + +// DecodeSyntax disassembles the text segment range [start, end), calling f for +// each instruction with Go assembler syntax and native (GNU) syntax separately. +// +// This is a lensm addition, re-applied on every `go generate` because upstream +// Decode only yields a single combined syntax. It relies on Decode's gnuAsm=true +// format of "%-36s // %s" (goText // gnuText) to split the two apart. +func (d *Disasm) DecodeSyntax(start, end uint64, relocs []objfile.Reloc, f func(pc, size uint64, file string, line int, goText, nativeText string)) { + if start < d.textStart { + start = d.textStart + } + if end > d.textEnd { + end = d.textEnd + } + code := d.text[:end-d.textStart] + lookup := d.lookup + for pc := start; pc < end; { + i := pc - d.textStart + combined, size := d.disasm(code[i:], pc, lookup, d.byteOrder, true) + goText, nativeText := combined, combined + if j := strings.Index(combined, " // "); j >= 0 { + goText = strings.TrimRight(combined[:j], " ") + nativeText = combined[j+len(" // "):] + } + file, line, _ := d.pcln.PCToLine(pc) + reloc := "" + sep := "\t" + for len(relocs) > 0 && relocs[0].Addr < i+uint64(size) { + reloc += sep + relocs[0].Stringer.String(pc-start) + sep = " " + relocs = relocs[1:] + } + f(pc, uint64(size), file, line, goText+reloc, nativeText+reloc) + pc += uint64(size) + } +} diff --git a/internal/go/src/abi/abi.go b/internal/go/src/abi/abi.go new file mode 100755 index 0000000..a3a7c50 --- /dev/null +++ b/internal/go/src/abi/abi.go @@ -0,0 +1,104 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +import ( + "loov.dev/lensm/internal/go/src/goarch" + "unsafe" +) + +// RegArgs is a struct that has space for each argument +// and return value register on the current architecture. +// +// Assembly code knows the layout of the first two fields +// of RegArgs. +// +// RegArgs also contains additional space to hold pointers +// when it may not be safe to keep them only in the integer +// register space otherwise. +type RegArgs struct { + // Values in these slots should be precisely the bit-by-bit + // representation of how they would appear in a register. + // + // This means that on big endian arches, integer values should + // be in the top bits of the slot. Floats are usually just + // directly represented, but some architectures treat narrow + // width floating point values specially (e.g. they're promoted + // first, or they need to be NaN-boxed). + Ints [IntArgRegs]uintptr // untyped integer registers + Floats [FloatArgRegs]uint64 // untyped float registers + + // Fields above this point are known to assembly. + + // Ptrs is a space that duplicates Ints but with pointer type, + // used to make pointers passed or returned in registers + // visible to the GC by making the type unsafe.Pointer. + Ptrs [IntArgRegs]unsafe.Pointer + + // ReturnIsPtr is a bitmap that indicates which registers + // contain or will contain pointers on the return path from + // a reflectcall. The i'th bit indicates whether the i'th + // register contains or will contain a valid Go pointer. + ReturnIsPtr IntArgRegBitmap +} + +func (r *RegArgs) Dump() { + print("Ints:") + for _, x := range r.Ints { + print(" ", x) + } + println() + print("Floats:") + for _, x := range r.Floats { + print(" ", x) + } + println() + print("Ptrs:") + for _, x := range r.Ptrs { + print(" ", x) + } + println() +} + +// IntRegArgAddr returns a pointer inside of r.Ints[reg] that is appropriately +// offset for an argument of size argSize. +// +// argSize must be non-zero, fit in a register, and a power-of-two. +// +// This method is a helper for dealing with the endianness of different CPU +// architectures, since sub-word-sized arguments in big endian architectures +// need to be "aligned" to the upper edge of the register to be interpreted +// by the CPU correctly. +func (r *RegArgs) IntRegArgAddr(reg int, argSize uintptr) unsafe.Pointer { + if argSize > goarch.PtrSize || argSize == 0 || argSize&(argSize-1) != 0 { + panic("invalid argSize") + } + offset := uintptr(0) + if goarch.BigEndian { + offset = goarch.PtrSize - argSize + } + return unsafe.Pointer(uintptr(unsafe.Pointer(&r.Ints[reg])) + offset) +} + +// IntArgRegBitmap is a bitmap large enough to hold one bit per +// integer argument/return register. +type IntArgRegBitmap [(IntArgRegs + 7) / 8]uint8 + +// Set sets the i'th bit of the bitmap to 1. +func (b *IntArgRegBitmap) Set(i int) { + b[i/8] |= uint8(1) << (i % 8) +} + +// Get returns whether the i'th bit of the bitmap is set. +// +// nosplit because it's called in extremely sensitive contexts, like +// on the reflectcall return path. +// +//go:nosplit +func (b *IntArgRegBitmap) Get(i int) bool { + // Compute p=&b[i/8], but without a bounds check. We don't have the stack for it. + p := (*byte)(unsafe.Pointer(uintptr(unsafe.Pointer(b)) + uintptr(i/8))) + return *p&(uint8(1)<<(i%8)) != 0 +} diff --git a/internal/go/src/abi/abi_amd64.go b/internal/go/src/abi/abi_amd64.go new file mode 100755 index 0000000..d3c5678 --- /dev/null +++ b/internal/go/src/abi/abi_amd64.go @@ -0,0 +1,18 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +const ( + // See abi_generic.go. + + // RAX, RBX, RCX, RDI, RSI, R8, R9, R10, R11. + IntArgRegs = 9 + + // X0 -> X14. + FloatArgRegs = 15 + + // We use SSE2 registers which support 64-bit float operations. + EffectiveFloatRegSize = 8 +) diff --git a/internal/go/src/abi/abi_arm64.go b/internal/go/src/abi/abi_arm64.go new file mode 100755 index 0000000..4dc5143 --- /dev/null +++ b/internal/go/src/abi/abi_arm64.go @@ -0,0 +1,17 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +const ( + // See abi_generic.go. + + // R0 - R15. + IntArgRegs = 16 + + // F0 - F15. + FloatArgRegs = 16 + + EffectiveFloatRegSize = 8 +) diff --git a/internal/go/src/abi/abi_generic.go b/internal/go/src/abi/abi_generic.go new file mode 100755 index 0000000..a08d320 --- /dev/null +++ b/internal/go/src/abi/abi_generic.go @@ -0,0 +1,38 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !goexperiment.regabiargs && !amd64 && !arm64 && !loong64 && !ppc64 && !ppc64le && !riscv64 + +package abi + +const ( + // ABI-related constants. + // + // In the generic case, these are all zero + // which lets them gracefully degrade to ABI0. + + // IntArgRegs is the number of registers dedicated + // to passing integer argument values. Result registers are identical + // to argument registers, so this number is used for those too. + IntArgRegs = 0 + + // FloatArgRegs is the number of registers dedicated + // to passing floating-point argument values. Result registers are + // identical to argument registers, so this number is used for + // those too. + FloatArgRegs = 0 + + // EffectiveFloatRegSize describes the width of floating point + // registers on the current platform from the ABI's perspective. + // + // Since Go only supports 32-bit and 64-bit floating point primitives, + // this number should be either 0, 4, or 8. 0 indicates no floating + // point registers for the ABI or that floating point values will be + // passed via the softfloat ABI. + // + // For platforms that support larger floating point register widths, + // such as x87's 80-bit "registers" (not that we support x87 currently), + // use 8. + EffectiveFloatRegSize = 0 +) diff --git a/internal/go/src/abi/abi_loong64.go b/internal/go/src/abi/abi_loong64.go new file mode 100755 index 0000000..10ad898 --- /dev/null +++ b/internal/go/src/abi/abi_loong64.go @@ -0,0 +1,17 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +const ( + // See abi_generic.go. + + // R4 - R19 + IntArgRegs = 16 + + // F0 - F15 + FloatArgRegs = 16 + + EffectiveFloatRegSize = 8 +) diff --git a/internal/go/src/abi/abi_ppc64x.go b/internal/go/src/abi/abi_ppc64x.go new file mode 100755 index 0000000..73416d7 --- /dev/null +++ b/internal/go/src/abi/abi_ppc64x.go @@ -0,0 +1,19 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ppc64 || ppc64le + +package abi + +const ( + // See abi_generic.go. + + // R3 - R10, R14 - R17. + IntArgRegs = 12 + + // F1 - F12. + FloatArgRegs = 12 + + EffectiveFloatRegSize = 8 +) diff --git a/internal/go/src/abi/abi_riscv64.go b/internal/go/src/abi/abi_riscv64.go new file mode 100755 index 0000000..2bcd9d6 --- /dev/null +++ b/internal/go/src/abi/abi_riscv64.go @@ -0,0 +1,17 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +const ( + // See abi_generic.go. + + // X8 - X23 + IntArgRegs = 16 + + // F8 - F23. + FloatArgRegs = 16 + + EffectiveFloatRegSize = 8 +) diff --git a/internal/go/src/abi/abi_s390x.go b/internal/go/src/abi/abi_s390x.go new file mode 100755 index 0000000..9b07f27 --- /dev/null +++ b/internal/go/src/abi/abi_s390x.go @@ -0,0 +1,19 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build goexperiment.regabiargs + +package abi + +const ( + // See abi_generic.go. + + // R2 - R9. + IntArgRegs = 8 + + // F0 - F15 + FloatArgRegs = 16 + + EffectiveFloatRegSize = 8 +) diff --git a/internal/go/src/abi/bounds.go b/internal/go/src/abi/bounds.go new file mode 100755 index 0000000..d685980 --- /dev/null +++ b/internal/go/src/abi/bounds.go @@ -0,0 +1,113 @@ +// Copyright 2025 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +// This type and constants are for encoding different +// kinds of bounds check failures. +type BoundsErrorCode uint8 + +const ( + BoundsIndex BoundsErrorCode = iota // s[x], 0 <= x < len(s) failed + BoundsSliceAlen // s[?:x], 0 <= x <= len(s) failed + BoundsSliceAcap // s[?:x], 0 <= x <= cap(s) failed + BoundsSliceB // s[x:y], 0 <= x <= y failed (but boundsSliceA didn't happen) + BoundsSlice3Alen // s[?:?:x], 0 <= x <= len(s) failed + BoundsSlice3Acap // s[?:?:x], 0 <= x <= cap(s) failed + BoundsSlice3B // s[?:x:y], 0 <= x <= y failed (but boundsSlice3A didn't happen) + BoundsSlice3C // s[x:y:?], 0 <= x <= y failed (but boundsSlice3A/B didn't happen) + BoundsConvert // (*[x]T)(s), 0 <= x <= len(s) failed + numBoundsCodes +) + +const ( + BoundsMaxReg = 15 + BoundsMaxConst = 31 +) + +// Here's how we encode PCDATA_PanicBounds entries: + +// We allow 16 registers (0-15) and 32 constants (0-31). +// Encode the following constant c: +// bits use +// ----------------------------- +// 0 x is in a register +// 1 y is in a register +// +// if x is in a register +// 2 x is signed +// [3:6] x's register number +// else +// [2:6] x's constant value +// +// if y is in a register +// [7:10] y's register number +// else +// [7:11] y's constant value +// +// The final integer is c * numBoundsCode + code + +// TODO: 32-bit + +// Encode bounds failure information into an integer for PCDATA_PanicBounds. +// Register numbers must be in 0-15. Constants must be in 0-31. +func BoundsEncode(code BoundsErrorCode, signed, xIsReg, yIsReg bool, xVal, yVal int) int { + c := int(0) + if xIsReg { + c |= 1 << 0 + if signed { + c |= 1 << 2 + } + if xVal < 0 || xVal > BoundsMaxReg { + panic("bad xReg") + } + c |= xVal << 3 + } else { + if xVal < 0 || xVal > BoundsMaxConst { + panic("bad xConst") + } + c |= xVal << 2 + } + if yIsReg { + c |= 1 << 1 + if yVal < 0 || yVal > BoundsMaxReg { + panic("bad yReg") + } + c |= yVal << 7 + } else { + if yVal < 0 || yVal > BoundsMaxConst { + panic("bad yConst") + } + c |= yVal << 7 + } + return c*int(numBoundsCodes) + int(code) +} +func BoundsDecode(v int) (code BoundsErrorCode, signed, xIsReg, yIsReg bool, xVal, yVal int) { + code = BoundsErrorCode(v % int(numBoundsCodes)) + c := v / int(numBoundsCodes) + xIsReg = c&1 != 0 + c >>= 1 + yIsReg = c&1 != 0 + c >>= 1 + if xIsReg { + signed = c&1 != 0 + c >>= 1 + xVal = c & 0xf + c >>= 4 + } else { + xVal = c & 0x1f + c >>= 5 + } + if yIsReg { + yVal = c & 0xf + c >>= 4 + } else { + yVal = c & 0x1f + c >>= 5 + } + if c != 0 { + panic("BoundsDecode decoding error") + } + return +} diff --git a/internal/go/src/abi/compiletype.go b/internal/go/src/abi/compiletype.go new file mode 100755 index 0000000..f00a69c --- /dev/null +++ b/internal/go/src/abi/compiletype.go @@ -0,0 +1,28 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +// These functions are the build-time version of the Go type data structures. + +// Their contents must be kept in sync with their definitions. +// Because the host and target type sizes can differ, the compiler and +// linker cannot use the host information that they might get from +// either unsafe.Sizeof and Alignof, nor runtime, reflect, or reflectlite. + +// CommonSize returns sizeof(Type) for a compilation target with a given ptrSize +func CommonSize(ptrSize int) int { return 4*ptrSize + 8 + 8 } + +// StructFieldSize returns sizeof(StructField) for a compilation target with a given ptrSize +func StructFieldSize(ptrSize int) int { return 3 * ptrSize } + +// UncommonSize returns sizeof(UncommonType). This currently does not depend on ptrSize. +// This exported function is in an internal package, so it may change to depend on ptrSize in the future. +func UncommonSize() uint64 { return 4 + 2 + 2 + 4 + 4 } + +// TFlagOff returns the offset of Type.TFlag for a compilation target with a given ptrSize +func TFlagOff(ptrSize int) int { return 2*ptrSize + 4 } + +// ITabTypeOff returns the offset of ITab.Type for a compilation target with a given ptrSize +func ITabTypeOff(ptrSize int) int { return ptrSize } diff --git a/internal/go/src/abi/escape.go b/internal/go/src/abi/escape.go new file mode 100755 index 0000000..d37be01 --- /dev/null +++ b/internal/go/src/abi/escape.go @@ -0,0 +1,65 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +import "unsafe" + +// NoEscape hides the pointer p from escape analysis, preventing it +// from escaping to the heap. It compiles down to nothing. +// +// WARNING: This is very subtle to use correctly. The caller must +// ensure that it's truly safe for p to not escape to the heap by +// maintaining runtime pointer invariants (for example, that globals +// and the heap may not generally point into a stack). +// +//go:nosplit +//go:nocheckptr +func NoEscape(p unsafe.Pointer) unsafe.Pointer { + x := uintptr(p) + return unsafe.Pointer(x ^ 0) +} + +var alwaysFalse bool +var escapeSink any + +// Escape forces any pointers in x to escape to the heap. +func Escape[T any](x T) T { + if alwaysFalse { + escapeSink = x + } + return x +} + +// EscapeNonString forces v to be on the heap, if v contains a +// non-string pointer. +// +// This is used in hash/maphash.Comparable. We cannot hash pointers +// to local variables on stack, as their addresses might change on +// stack growth. Strings are okay as the hash depends on only the +// content, not the pointer. +// +// This is essentially +// +// if hasNonStringPointers(T) { Escape(v) } +// +// Implemented as a compiler intrinsic. +func EscapeNonString[T any](v T) { panic("intrinsic") } + +// EscapeToResultNonString models a data flow edge from v to the result, +// if v contains a non-string pointer. If v contains only string pointers, +// it returns a copy of v, but is not modeled as a data flow edge +// from the escape analysis's perspective. +// +// This is used in unique.clone, to model the data flow edge on the +// value with strings excluded, because strings are cloned (by +// content). +// +// TODO: probably we should define this as a intrinsic and EscapeNonString +// could just be "heap = EscapeToResultNonString(v)". This way we can model +// an edge to the result but not necessarily heap. +func EscapeToResultNonString[T any](v T) T { + EscapeNonString(v) + return *(*T)(NoEscape(unsafe.Pointer(&v))) +} diff --git a/internal/go/src/abi/funcpc.go b/internal/go/src/abi/funcpc.go new file mode 100755 index 0000000..54b0735 --- /dev/null +++ b/internal/go/src/abi/funcpc.go @@ -0,0 +1,31 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !gccgo + +package abi + +// FuncPC* intrinsics. +// +// CAREFUL: In programs with plugins, FuncPC* can return different values +// for the same function (because there are actually multiple copies of +// the same function in the address space). To be safe, don't use the +// results of this function in any == expression. It is only safe to +// use the result as an address at which to start executing code. + +// FuncPCABI0 returns the entry PC of the function f, which must be a +// direct reference of a function defined as ABI0. Otherwise it is a +// compile-time error. +// +// Implemented as a compile intrinsic. +func FuncPCABI0(f any) uintptr + +// FuncPCABIInternal returns the entry PC of the function f. If f is a +// direct reference of a function, it must be defined as ABIInternal. +// Otherwise it is a compile-time error. If f is not a direct reference +// of a defined function, it assumes that f is a func value. Otherwise +// the behavior is undefined. +// +// Implemented as a compile intrinsic. +func FuncPCABIInternal(f any) uintptr diff --git a/internal/go/src/abi/funcpc_gccgo.go b/internal/go/src/abi/funcpc_gccgo.go new file mode 100755 index 0000000..ad5fa52 --- /dev/null +++ b/internal/go/src/abi/funcpc_gccgo.go @@ -0,0 +1,21 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// For bootstrapping with gccgo. + +//go:build gccgo + +package abi + +import "unsafe" + +func FuncPCABI0(f interface{}) uintptr { + words := (*[2]unsafe.Pointer)(unsafe.Pointer(&f)) + return *(*uintptr)(unsafe.Pointer(words[1])) +} + +func FuncPCABIInternal(f interface{}) uintptr { + words := (*[2]unsafe.Pointer)(unsafe.Pointer(&f)) + return *(*uintptr)(unsafe.Pointer(words[1])) +} diff --git a/internal/go/src/abi/iface.go b/internal/go/src/abi/iface.go new file mode 100755 index 0000000..f53d7e1 --- /dev/null +++ b/internal/go/src/abi/iface.go @@ -0,0 +1,41 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +import "unsafe" + +// The first word of every non-empty interface type contains an *ITab. +// It records the underlying concrete type (Type), the interface type it +// is implementing (Inter), and some ancillary information. +// +// allocated in non-garbage-collected memory +type ITab struct { + Inter *InterfaceType + Type *Type + Hash uint32 // copy of Type.Hash. Used for type switches. + Fun [1]uintptr // variable sized. fun[0]==0 means Type does not implement Inter. +} + +// EmptyInterface describes the layout of a "interface{}" or a "any." +// These are represented differently than non-empty interface, as the first +// word always points to an abi.Type. +type EmptyInterface struct { + Type *Type + Data unsafe.Pointer +} + +// NonEmptyInterface describes the layout of an interface that contains any methods. +type NonEmptyInterface struct { + ITab *ITab + Data unsafe.Pointer +} + +// CommonInterface describes the layout of both [EmptyInterface] and [NonEmptyInterface]. +type CommonInterface struct { + // Either an *ITab or a *Type, unexported to avoid accidental use. + _ unsafe.Pointer + + Data unsafe.Pointer +} diff --git a/internal/go/src/abi/map.go b/internal/go/src/abi/map.go new file mode 100755 index 0000000..4476dda --- /dev/null +++ b/internal/go/src/abi/map.go @@ -0,0 +1,64 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +import ( + "unsafe" +) + +// Map constants common to several packages +// runtime/runtime-gdb.py:MapTypePrinter contains its own copy +const ( + // Number of bits in the group.slot count. + MapGroupSlotsBits = 3 + + // Number of slots in a group. + MapGroupSlots = 1 << MapGroupSlotsBits // 8 + + // Maximum key or elem size to keep inline (instead of mallocing per element). + // Must fit in a uint8. + MapMaxKeyBytes = 128 + MapMaxElemBytes = 128 + + ctrlEmpty = 0b10000000 + bitsetLSB = 0x0101010101010101 + + // Value of control word with all empty slots. + MapCtrlEmpty = bitsetLSB * uint64(ctrlEmpty) +) + +type MapType struct { + Type + Key *Type + Elem *Type + Group *Type // internal type representing a slot group + // function for hashing keys (ptr to key, seed) -> hash + Hasher func(unsafe.Pointer, uintptr) uintptr + GroupSize uintptr // == Group.Size_ + SlotSize uintptr // size of key/elem slot + ElemOff uintptr // offset of elem in key/elem slot + Flags uint32 +} + +// Flag values +const ( + MapNeedKeyUpdate = 1 << iota + MapHashMightPanic + MapIndirectKey + MapIndirectElem +) + +func (mt *MapType) NeedKeyUpdate() bool { // true if we need to update key on an overwrite + return mt.Flags&MapNeedKeyUpdate != 0 +} +func (mt *MapType) HashMightPanic() bool { // true if hash function might panic + return mt.Flags&MapHashMightPanic != 0 +} +func (mt *MapType) IndirectKey() bool { // store ptr to key instead of key itself + return mt.Flags&MapIndirectKey != 0 +} +func (mt *MapType) IndirectElem() bool { // store ptr to elem instead of elem itself + return mt.Flags&MapIndirectElem != 0 +} diff --git a/internal/go/src/abi/rangefuncconsts.go b/internal/go/src/abi/rangefuncconsts.go new file mode 100755 index 0000000..4e9248d --- /dev/null +++ b/internal/go/src/abi/rangefuncconsts.go @@ -0,0 +1,18 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +type RF_State int + +// These constants are shared between the compiler, which uses them for state functions +// and panic indicators, and the runtime, which turns them into more meaningful strings +// For best code generation, RF_DONE and RF_READY should be 0 and 1. +const ( + RF_DONE = RF_State(iota) // body of loop has exited in a non-panic way + RF_READY // body of loop has not exited yet, is not running -- this is not a panic index + RF_PANIC // body of loop is either currently running, or has panicked + RF_EXHAUSTED // iterator function return, i.e., sequence is "exhausted" + RF_MISSING_PANIC = 4 // body of loop panicked but iterator function defer-recovered it away +) diff --git a/internal/go/src/abi/runtime.go b/internal/go/src/abi/runtime.go new file mode 100755 index 0000000..9b91cdf --- /dev/null +++ b/internal/go/src/abi/runtime.go @@ -0,0 +1,8 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +// ZeroValSize is the size in bytes of runtime.zeroVal. +const ZeroValSize = 1024 diff --git a/internal/go/src/abi/stack.go b/internal/go/src/abi/stack.go new file mode 100755 index 0000000..8e3327e --- /dev/null +++ b/internal/go/src/abi/stack.go @@ -0,0 +1,33 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +const ( + // StackNosplitBase is the base maximum number of bytes that a chain of + // NOSPLIT functions can use. + // + // This value must be multiplied by the stack guard multiplier, so do not + // use it directly. See runtime/stack.go:stackNosplit and + // cmd/internal/objabi/stack.go:StackNosplit. + StackNosplitBase = 800 + + // We have three different sequences for stack bounds checks, depending on + // whether the stack frame of a function is small, big, or huge. + + // After a stack split check the SP is allowed to be StackSmall bytes below + // the stack guard. + // + // Functions that need frames <= StackSmall can perform the stack check + // using a single comparison directly between the stack guard and the SP + // because we ensure that StackSmall bytes of stack space are available + // beyond the stack guard. + StackSmall = 128 + + // Functions that need frames <= StackBig can assume that neither + // SP-framesize nor stackGuard-StackSmall will underflow, and thus use a + // more efficient check. In order to ensure this, StackBig must be <= the + // size of the unmapped space at zero. + StackBig = 4096 +) diff --git a/internal/go/src/abi/stub.s b/internal/go/src/abi/stub.s new file mode 100755 index 0000000..5bad98d --- /dev/null +++ b/internal/go/src/abi/stub.s @@ -0,0 +1,7 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file silences errors about body-less functions +// that are provided by intrinsics in the latest version of the compiler, +// but may not be known to the bootstrap compiler. diff --git a/internal/go/src/abi/switch.go b/internal/go/src/abi/switch.go new file mode 100755 index 0000000..81c2b5b --- /dev/null +++ b/internal/go/src/abi/switch.go @@ -0,0 +1,58 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +import "loov.dev/lensm/internal/go/src/goarch" + +type InterfaceSwitch struct { + Cache *InterfaceSwitchCache + NCases int + + // Array of NCases elements. + // Each case must be a non-empty interface type. + Cases [1]*InterfaceType +} + +type InterfaceSwitchCache struct { + Mask uintptr // mask for index. Must be a power of 2 minus 1 + Entries [1]InterfaceSwitchCacheEntry // Mask+1 entries total +} + +type InterfaceSwitchCacheEntry struct { + // type of source value (a *Type) + Typ uintptr + // case # to dispatch to + Case int + // itab to use for resulting case variable (a *runtime.itab) + Itab uintptr +} + +func UseInterfaceSwitchCache(arch goarch.ArchFamilyType) bool { + // We need an atomic load instruction to make the cache multithreaded-safe. + // (AtomicLoadPtr needs to be implemented in cmd/compile/internal/ssa/_gen/ARCH.rules.) + switch arch { + case goarch.AMD64, goarch.ARM64, goarch.LOONG64, goarch.MIPS, goarch.MIPS64, goarch.PPC64, goarch.RISCV64, goarch.S390X: + return true + default: + return false + } +} + +type TypeAssert struct { + Cache *TypeAssertCache + Inter *InterfaceType + CanFail bool +} +type TypeAssertCache struct { + Mask uintptr + Entries [1]TypeAssertCacheEntry +} +type TypeAssertCacheEntry struct { + // type of source value (a *runtime._type) + Typ uintptr + // itab to use for result (a *runtime.itab) + // nil if CanFail is set and conversion would fail. + Itab uintptr +} diff --git a/internal/go/src/abi/symtab.go b/internal/go/src/abi/symtab.go new file mode 100755 index 0000000..10033e7 --- /dev/null +++ b/internal/go/src/abi/symtab.go @@ -0,0 +1,143 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +// PCLnTabMagic is the version at the start of the PC/line table. +// This is the start of the .pclntab section, and is also runtime.pcHeader. +// The magic numbers are chosen such that reading the value with +// a different endianness does not result in the same value. +// That lets us the magic number to determine the endianness. +type PCLnTabMagic uint32 + +const ( + // Initial PCLnTabMagic value used in Go 1.2 through Go 1.15. + Go12PCLnTabMagic PCLnTabMagic = 0xfffffffb + // PCLnTabMagic value used in Go 1.16 through Go 1.17. + // Several fields added to header (CL 241598). + Go116PCLnTabMagic PCLnTabMagic = 0xfffffffa + // PCLnTabMagic value used in Go 1.18 through Go 1.19. + // Entry PC of func data changed from address to offset (CL 351463). + Go118PCLnTabMagic PCLnTabMagic = 0xfffffff0 + // PCLnTabMagic value used in Go 1.20 and later. + // A ":" was added to generated symbol names (#37762). + Go120PCLnTabMagic PCLnTabMagic = 0xfffffff1 + + // CurrentPCLnTabMagic is the value emitted by the current toolchain. + // This is written by the linker to the pcHeader and read by the + // runtime and debug/gosym (and external tools like Delve). + // + // Change this value when updating the pclntab version. + // Changing this exported value is OK because is an + // internal package. + CurrentPCLnTabMagic = Go120PCLnTabMagic +) + +// A FuncFlag records bits about a function, passed to the runtime. +type FuncFlag uint8 + +const ( + // FuncFlagTopFrame indicates a function that appears at the top of its stack. + // The traceback routine stop at such a function and consider that a + // successful, complete traversal of the stack. + // Examples of TopFrame functions include goexit, which appears + // at the top of a user goroutine stack, and mstart, which appears + // at the top of a system goroutine stack. + FuncFlagTopFrame FuncFlag = 1 << iota + + // FuncFlagSPWrite indicates a function that writes an arbitrary value to SP + // (any write other than adding or subtracting a constant amount). + // The traceback routines cannot encode such changes into the + // pcsp tables, so the function traceback cannot safely unwind past + // SPWrite functions. Stopping at an SPWrite function is considered + // to be an incomplete unwinding of the stack. In certain contexts + // (in particular garbage collector stack scans) that is a fatal error. + FuncFlagSPWrite + + // FuncFlagAsm indicates that a function was implemented in assembly. + FuncFlagAsm +) + +// A FuncID identifies particular functions that need to be treated +// specially by the runtime. +// Note that in some situations involving plugins, there may be multiple +// copies of a particular special runtime function. +type FuncID uint8 + +const ( + // If you add a FuncID, you probably also want to add an entry to the map in + // ../../cmd/internal/objabi/funcid.go + + FuncIDNormal FuncID = iota // not a special function + FuncID_abort + FuncID_asmcgocall + FuncID_asyncPreempt + FuncID_cgocallback + FuncID_corostart + FuncID_debugCallV2 + FuncID_gcBgMarkWorker + FuncID_goexit + FuncID_gogo + FuncID_gopanic + FuncID_handleAsyncEvent + FuncID_mcall + FuncID_morestack + FuncID_mstart + FuncID_panicwrap + FuncID_rt0_go + FuncID_runtime_main + FuncID_runFinalizers + FuncID_runCleanups + FuncID_sigpanic + FuncID_systemstack + FuncID_systemstack_switch + FuncIDWrapper // any autogenerated code (hash/eq algorithms, method wrappers, etc.) +) + +// ArgsSizeUnknown is set in Func.argsize to mark all functions +// whose argument size is unknown (C vararg functions, and +// assembly code without an explicit specification). +// This value is generated by the compiler, assembler, or linker. +const ArgsSizeUnknown = -0x80000000 + +// IDs for PCDATA and FUNCDATA tables in Go binaries. +// +// These must agree with ../../../runtime/funcdata.h. +const ( + PCDATA_UnsafePoint = 0 + PCDATA_StackMapIndex = 1 + PCDATA_InlTreeIndex = 2 + PCDATA_ArgLiveIndex = 3 + PCDATA_PanicBounds = 4 + + FUNCDATA_ArgsPointerMaps = 0 + FUNCDATA_LocalsPointerMaps = 1 + FUNCDATA_StackObjects = 2 + FUNCDATA_InlTree = 3 + FUNCDATA_OpenCodedDeferInfo = 4 + FUNCDATA_ArgInfo = 5 + FUNCDATA_ArgLiveInfo = 6 + FUNCDATA_WrapInfo = 7 +) + +// Special values for the PCDATA_UnsafePoint table. +const ( + UnsafePointSafe = -1 // Safe for async preemption + UnsafePointUnsafe = -2 // Unsafe for async preemption + + // UnsafePointRestart1(2) apply on a sequence of instructions, within + // which if an async preemption happens, we should back off the PC + // to the start of the sequence when resuming. + // We need two so we can distinguish the start/end of the sequence + // in case that two sequences are next to each other. + UnsafePointRestart1 = -3 + UnsafePointRestart2 = -4 + + // Like UnsafePointRestart1, but back to function entry if async preempted. + UnsafePointRestartAtEntry = -5 +) + +const MINFUNC = 16 // minimum size for a function + +const FuncTabBucketSize = 256 * MINFUNC // size of bucket in the pc->func lookup table diff --git a/internal/go/src/abi/type.go b/internal/go/src/abi/type.go new file mode 100755 index 0000000..243b787 --- /dev/null +++ b/internal/go/src/abi/type.go @@ -0,0 +1,777 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package abi + +import ( + "unsafe" +) + +// Type is the runtime representation of a Go type. +// +// Be careful about accessing this type at build time, as the version +// of this type in the compiler/linker may not have the same layout +// as the version in the target binary, due to pointer width +// differences and any experiments. Use cmd/compile/internal/rttype +// or the functions in compiletype.go to access this type instead. +// (TODO: this admonition applies to every type in this package. +// Put it in some shared location?) +type Type struct { + Size_ uintptr + PtrBytes uintptr // number of (prefix) bytes in the type that can contain pointers + Hash uint32 // hash of type; avoids computation in hash tables + TFlag TFlag // extra type information flags + Align_ uint8 // alignment of variable with this type + FieldAlign_ uint8 // alignment of struct field with this type + Kind_ Kind // what kind of type this is (string, int, ...) + // function for comparing objects of this type + // (ptr to object A, ptr to object B) -> ==? + Equal func(unsafe.Pointer, unsafe.Pointer) bool + // GCData stores the GC type data for the garbage collector. + // Normally, GCData points to a bitmask that describes the + // ptr/nonptr fields of the type. The bitmask will have at + // least PtrBytes/ptrSize bits. + // If the TFlagGCMaskOnDemand bit is set, GCData is instead a + // **byte and the pointer to the bitmask is one dereference away. + // The runtime will build the bitmask if needed. + // (See runtime/type.go:getGCMask.) + // Note: multiple types may have the same value of GCData, + // including when TFlagGCMaskOnDemand is set. The types will, of course, + // have the same pointer layout (but not necessarily the same size). + GCData *byte + Str NameOff // string form + PtrToThis TypeOff // type for pointer to this type, may be zero +} + +// A Kind represents the specific kind of type that a Type represents. +// The zero Kind is not a valid kind. +type Kind uint8 + +const ( + Invalid Kind = iota + Bool + Int + Int8 + Int16 + Int32 + Int64 + Uint + Uint8 + Uint16 + Uint32 + Uint64 + Uintptr + Float32 + Float64 + Complex64 + Complex128 + Array + Chan + Func + Interface + Map + Pointer + Slice + String + Struct + UnsafePointer +) + +// TFlag is used by a Type to signal what extra type information is +// available in the memory directly following the Type value. +type TFlag uint8 + +const ( + // TFlagUncommon means that there is a data with a type, UncommonType, + // just beyond the shared-per-type common data. That is, the data + // for struct types will store their UncommonType at one offset, the + // data for interface types will store their UncommonType at a different + // offset. UncommonType is always accessed via a pointer that is computed + // using trust-us-we-are-the-implementors pointer arithmetic. + // + // For example, if t.Kind() == Struct and t.tflag&TFlagUncommon != 0, + // then t has UncommonType data and it can be accessed as: + // + // type structTypeUncommon struct { + // structType + // u UncommonType + // } + // u := &(*structTypeUncommon)(unsafe.Pointer(t)).u + TFlagUncommon TFlag = 1 << 0 + + // TFlagExtraStar means the name in the str field has an + // extraneous '*' prefix. This is because for most types T in + // a program, the type *T also exists and reusing the str data + // saves binary size. + TFlagExtraStar TFlag = 1 << 1 + + // TFlagNamed means the type has a name. + TFlagNamed TFlag = 1 << 2 + + // TFlagRegularMemory means that equal and hash functions can treat + // this type as a single region of t.size bytes. + TFlagRegularMemory TFlag = 1 << 3 + + // TFlagGCMaskOnDemand means that the GC pointer bitmask will be + // computed on demand at runtime instead of being precomputed at + // compile time. If this flag is set, the GCData field effectively + // has type **byte instead of *byte. The runtime will store a + // pointer to the GC pointer bitmask in *GCData. + TFlagGCMaskOnDemand TFlag = 1 << 4 + + // TFlagDirectIface means that a value of this type is stored directly + // in the data field of an interface, instead of indirectly. + // This flag is just a cached computation of Size_ == PtrBytes == goarch.PtrSize. + TFlagDirectIface TFlag = 1 << 5 + + // Leaving this breadcrumb behind for dlv. It should not be used, and no + // Kind should be big enough to set this bit. + KindDirectIface Kind = 1 << 5 +) + +// NameOff is the offset to a name from moduledata.types. See resolveNameOff in runtime. +type NameOff int32 + +// TypeOff is the offset to a type from moduledata.types. See resolveTypeOff in runtime. +type TypeOff int32 + +// TextOff is an offset from the top of a text section. See (rtype).textOff in runtime. +type TextOff int32 + +// String returns the name of k. +func (k Kind) String() string { + if int(k) < len(kindNames) { + return kindNames[k] + } + return kindNames[0] +} + +var kindNames = []string{ + Invalid: "invalid", + Bool: "bool", + Int: "int", + Int8: "int8", + Int16: "int16", + Int32: "int32", + Int64: "int64", + Uint: "uint", + Uint8: "uint8", + Uint16: "uint16", + Uint32: "uint32", + Uint64: "uint64", + Uintptr: "uintptr", + Float32: "float32", + Float64: "float64", + Complex64: "complex64", + Complex128: "complex128", + Array: "array", + Chan: "chan", + Func: "func", + Interface: "interface", + Map: "map", + Pointer: "ptr", + Slice: "slice", + String: "string", + Struct: "struct", + UnsafePointer: "unsafe.Pointer", +} + +// TypeOf returns the abi.Type of some value. +func TypeOf(a any) *Type { + eface := *(*EmptyInterface)(unsafe.Pointer(&a)) + // Types are either static (for compiler-created types) or + // heap-allocated but always reachable (for reflection-created + // types, held in the central map). So there is no need to + // escape types. noescape here help avoid unnecessary escape + // of v. + return (*Type)(NoEscape(unsafe.Pointer(eface.Type))) +} + +// TypeFor returns the abi.Type for a type parameter. +func TypeFor[T any]() *Type { + return (*PtrType)(unsafe.Pointer(TypeOf((*T)(nil)))).Elem +} + +func (t *Type) Kind() Kind { return t.Kind_ } + +func (t *Type) HasName() bool { + return t.TFlag&TFlagNamed != 0 +} + +// Pointers reports whether t contains pointers. +func (t *Type) Pointers() bool { return t.PtrBytes != 0 } + +// IsDirectIface reports whether t is stored directly in an interface value. +func (t *Type) IsDirectIface() bool { + return t.TFlag&TFlagDirectIface != 0 +} + +func (t *Type) GcSlice(begin, end uintptr) []byte { + if t.TFlag&TFlagGCMaskOnDemand != 0 { + panic("GcSlice can't handle on-demand gcdata types") + } + return unsafe.Slice(t.GCData, int(end))[begin:] +} + +// Method on non-interface type +type Method struct { + Name NameOff // name of method + Mtyp TypeOff // method type (without receiver) + Ifn TextOff // fn used in interface call (one-word receiver) + Tfn TextOff // fn used for normal method call +} + +// UncommonType is present only for defined types or types with methods +// (if T is a defined type, the uncommonTypes for T and *T have methods). +// Using a pointer to this struct reduces the overall size required +// to describe a non-defined type with no methods. +type UncommonType struct { + PkgPath NameOff // import path; empty for built-in types like int, string + Mcount uint16 // number of methods + Xcount uint16 // number of exported methods + Moff uint32 // offset from this uncommontype to [mcount]Method + _ uint32 // unused +} + +func (t *UncommonType) Methods() []Method { + if t.Mcount == 0 { + return nil + } + return (*[1 << 16]Method)(addChecked(unsafe.Pointer(t), uintptr(t.Moff), "t.mcount > 0"))[:t.Mcount:t.Mcount] +} + +func (t *UncommonType) ExportedMethods() []Method { + if t.Xcount == 0 { + return nil + } + return (*[1 << 16]Method)(addChecked(unsafe.Pointer(t), uintptr(t.Moff), "t.xcount > 0"))[:t.Xcount:t.Xcount] +} + +// addChecked returns p+x. +// +// The whySafe string is ignored, so that the function still inlines +// as efficiently as p+x, but all call sites should use the string to +// record why the addition is safe, which is to say why the addition +// does not cause x to advance to the very end of p's allocation +// and therefore point incorrectly at the next block in memory. +func addChecked(p unsafe.Pointer, x uintptr, whySafe string) unsafe.Pointer { + return unsafe.Pointer(uintptr(p) + x) +} + +// Imethod represents a method on an interface type +type Imethod struct { + Name NameOff // name of method + Typ TypeOff // .(*FuncType) underneath +} + +// ArrayType represents a fixed array type. +type ArrayType struct { + Type + Elem *Type // array element type + Slice *Type // slice type + Len uintptr +} + +// Len returns the length of t if t is an array type, otherwise 0 +func (t *Type) Len() int { + if t.Kind() == Array { + return int((*ArrayType)(unsafe.Pointer(t)).Len) + } + return 0 +} + +func (t *Type) Common() *Type { + return t +} + +type ChanDir int + +const ( + RecvDir ChanDir = 1 << iota // <-chan + SendDir // chan<- + BothDir = RecvDir | SendDir // chan + InvalidDir ChanDir = 0 +) + +// ChanType represents a channel type +type ChanType struct { + Type + Elem *Type + Dir ChanDir +} + +type structTypeUncommon struct { + StructType + u UncommonType +} + +// ChanDir returns the direction of t if t is a channel type, otherwise InvalidDir (0). +func (t *Type) ChanDir() ChanDir { + if t.Kind() == Chan { + ch := (*ChanType)(unsafe.Pointer(t)) + return ch.Dir + } + return InvalidDir +} + +// Uncommon returns a pointer to T's "uncommon" data if there is any, otherwise nil +func (t *Type) Uncommon() *UncommonType { + if t.TFlag&TFlagUncommon == 0 { + return nil + } + switch t.Kind() { + case Struct: + return &(*structTypeUncommon)(unsafe.Pointer(t)).u + case Pointer: + type u struct { + PtrType + u UncommonType + } + return &(*u)(unsafe.Pointer(t)).u + case Func: + type u struct { + FuncType + u UncommonType + } + return &(*u)(unsafe.Pointer(t)).u + case Slice: + type u struct { + SliceType + u UncommonType + } + return &(*u)(unsafe.Pointer(t)).u + case Array: + type u struct { + ArrayType + u UncommonType + } + return &(*u)(unsafe.Pointer(t)).u + case Chan: + type u struct { + ChanType + u UncommonType + } + return &(*u)(unsafe.Pointer(t)).u + case Map: + type u struct { + MapType + u UncommonType + } + return &(*u)(unsafe.Pointer(t)).u + case Interface: + type u struct { + InterfaceType + u UncommonType + } + return &(*u)(unsafe.Pointer(t)).u + default: + type u struct { + Type + u UncommonType + } + return &(*u)(unsafe.Pointer(t)).u + } +} + +// Elem returns the element type for t if t is an array, channel, map, pointer, or slice, otherwise nil. +func (t *Type) Elem() *Type { + switch t.Kind() { + case Array: + tt := (*ArrayType)(unsafe.Pointer(t)) + return tt.Elem + case Chan: + tt := (*ChanType)(unsafe.Pointer(t)) + return tt.Elem + case Map: + tt := (*MapType)(unsafe.Pointer(t)) + return tt.Elem + case Pointer: + tt := (*PtrType)(unsafe.Pointer(t)) + return tt.Elem + case Slice: + tt := (*SliceType)(unsafe.Pointer(t)) + return tt.Elem + } + return nil +} + +// StructType returns t cast to a *StructType, or nil if its tag does not match. +func (t *Type) StructType() *StructType { + if t.Kind() != Struct { + return nil + } + return (*StructType)(unsafe.Pointer(t)) +} + +// MapType returns t cast to a *MapType, or nil if its tag does not match. +func (t *Type) MapType() *MapType { + if t.Kind() != Map { + return nil + } + return (*MapType)(unsafe.Pointer(t)) +} + +// ArrayType returns t cast to a *ArrayType, or nil if its tag does not match. +func (t *Type) ArrayType() *ArrayType { + if t.Kind() != Array { + return nil + } + return (*ArrayType)(unsafe.Pointer(t)) +} + +// FuncType returns t cast to a *FuncType, or nil if its tag does not match. +func (t *Type) FuncType() *FuncType { + if t.Kind() != Func { + return nil + } + return (*FuncType)(unsafe.Pointer(t)) +} + +// InterfaceType returns t cast to a *InterfaceType, or nil if its tag does not match. +func (t *Type) InterfaceType() *InterfaceType { + if t.Kind() != Interface { + return nil + } + return (*InterfaceType)(unsafe.Pointer(t)) +} + +// Size returns the size of data with type t. +func (t *Type) Size() uintptr { return t.Size_ } + +// Align returns the alignment of data with type t. +func (t *Type) Align() int { return int(t.Align_) } + +func (t *Type) FieldAlign() int { return int(t.FieldAlign_) } + +type InterfaceType struct { + Type + PkgPath Name // import path + Methods []Imethod // sorted by hash +} + +func (t *Type) ExportedMethods() []Method { + ut := t.Uncommon() + if ut == nil { + return nil + } + return ut.ExportedMethods() +} + +func (t *Type) NumMethod() int { + if t.Kind() == Interface { + tt := (*InterfaceType)(unsafe.Pointer(t)) + return tt.NumMethod() + } + return len(t.ExportedMethods()) +} + +// NumMethod returns the number of interface methods in the type's method set. +func (t *InterfaceType) NumMethod() int { return len(t.Methods) } + +func (t *Type) Key() *Type { + if t.Kind() == Map { + return (*MapType)(unsafe.Pointer(t)).Key + } + return nil +} + +type SliceType struct { + Type + Elem *Type // slice element type +} + +// FuncType represents a function type. +// +// A *Type for each in and out parameter is stored in an array that +// directly follows the funcType (and possibly its uncommonType). So +// a function type with one method, one input, and one output is: +// +// struct { +// funcType +// uncommonType +// [2]*rtype // [0] is in, [1] is out +// } +type FuncType struct { + Type + InCount uint16 + OutCount uint16 // top bit is set if last input parameter is ... +} + +func (t *FuncType) In(i int) *Type { + return t.InSlice()[i] +} + +func (t *FuncType) NumIn() int { + return int(t.InCount) +} + +func (t *FuncType) NumOut() int { + return int(t.OutCount & (1<<15 - 1)) +} + +func (t *FuncType) Out(i int) *Type { + return (t.OutSlice()[i]) +} + +func (t *FuncType) InSlice() []*Type { + uadd := unsafe.Sizeof(*t) + if t.TFlag&TFlagUncommon != 0 { + uadd += unsafe.Sizeof(UncommonType{}) + } + if t.InCount == 0 { + return nil + } + return (*[1 << 16]*Type)(addChecked(unsafe.Pointer(t), uadd, "t.inCount > 0"))[:t.InCount:t.InCount] +} +func (t *FuncType) OutSlice() []*Type { + outCount := uint16(t.NumOut()) + if outCount == 0 { + return nil + } + uadd := unsafe.Sizeof(*t) + if t.TFlag&TFlagUncommon != 0 { + uadd += unsafe.Sizeof(UncommonType{}) + } + return (*[1 << 17]*Type)(addChecked(unsafe.Pointer(t), uadd, "outCount > 0"))[t.InCount : t.InCount+outCount : t.InCount+outCount] +} + +func (t *FuncType) IsVariadic() bool { + return t.OutCount&(1<<15) != 0 +} + +type PtrType struct { + Type + Elem *Type // pointer element (pointed at) type +} + +type StructField struct { + Name Name // name is always non-empty + Typ *Type // type of field + Offset uintptr // byte offset of field +} + +func (f *StructField) Embedded() bool { + return f.Name.IsEmbedded() +} + +type StructType struct { + Type + PkgPath Name + Fields []StructField +} + +// Name is an encoded type Name with optional extra data. +// +// The first byte is a bit field containing: +// +// 1<<0 the name is exported +// 1<<1 tag data follows the name +// 1<<2 pkgPath nameOff follows the name and tag +// 1<<3 the name is of an embedded (a.k.a. anonymous) field +// +// Following that, there is a varint-encoded length of the name, +// followed by the name itself. +// +// If tag data is present, it also has a varint-encoded length +// followed by the tag itself. +// +// If the import path follows, then 4 bytes at the end of +// the data form a nameOff. The import path is only set for concrete +// methods that are defined in a different package than their type. +// +// If a name starts with "*", then the exported bit represents +// whether the pointed to type is exported. +// +// Note: this encoding must match here and in: +// cmd/compile/internal/reflectdata/reflect.go +// cmd/link/internal/ld/decodesym.go + +type Name struct { + Bytes *byte +} + +// DataChecked does pointer arithmetic on n's Bytes, and that arithmetic is asserted to +// be safe for the reason in whySafe (which can appear in a backtrace, etc.) +func (n Name) DataChecked(off int, whySafe string) *byte { + return (*byte)(addChecked(unsafe.Pointer(n.Bytes), uintptr(off), whySafe)) +} + +// Data does pointer arithmetic on n's Bytes, and that arithmetic is asserted to +// be safe because the runtime made the call (other packages use DataChecked) +func (n Name) Data(off int) *byte { + return (*byte)(addChecked(unsafe.Pointer(n.Bytes), uintptr(off), "the runtime doesn't need to give you a reason")) +} + +// IsExported returns "is n exported?" +func (n Name) IsExported() bool { + return (*n.Bytes)&(1<<0) != 0 +} + +// HasTag returns true iff there is tag data following this name +func (n Name) HasTag() bool { + return (*n.Bytes)&(1<<1) != 0 +} + +// IsEmbedded returns true iff n is embedded (an anonymous field). +func (n Name) IsEmbedded() bool { + return (*n.Bytes)&(1<<3) != 0 +} + +// ReadVarint parses a varint as encoded by encoding/binary. +// It returns the number of encoded bytes and the encoded value. +func (n Name) ReadVarint(off int) (int, int) { + v := 0 + for i := 0; ; i++ { + x := *n.DataChecked(off+i, "read varint") + v += int(x&0x7f) << (7 * i) + if x&0x80 == 0 { + return i + 1, v + } + } +} + +// IsBlank indicates whether n is "_". +func (n Name) IsBlank() bool { + if n.Bytes == nil { + return false + } + _, l := n.ReadVarint(1) + return l == 1 && *n.Data(2) == '_' +} + +// writeVarint writes n to buf in varint form. Returns the +// number of bytes written. n must be nonnegative. +// Writes at most 10 bytes. +func writeVarint(buf []byte, n int) int { + for i := 0; ; i++ { + b := byte(n & 0x7f) + n >>= 7 + if n == 0 { + buf[i] = b + return i + 1 + } + buf[i] = b | 0x80 + } +} + +// Name returns the name of n, or empty if it does not actually have a name. +func (n Name) Name() string { + if n.Bytes == nil { + return "" + } + i, l := n.ReadVarint(1) + return unsafe.String(n.DataChecked(1+i, "non-empty string"), l) +} + +// Tag returns the tag string for n, or empty if there is none. +func (n Name) Tag() string { + if !n.HasTag() { + return "" + } + i, l := n.ReadVarint(1) + i2, l2 := n.ReadVarint(1 + i + l) + return unsafe.String(n.DataChecked(1+i+l+i2, "non-empty string"), l2) +} + +func NewName(n, tag string, exported, embedded bool) Name { + if len(n) >= 1<<29 { + panic("abi.NewName: name too long: " + n[:1024] + "...") + } + if len(tag) >= 1<<29 { + panic("abi.NewName: tag too long: " + tag[:1024] + "...") + } + var nameLen [10]byte + var tagLen [10]byte + nameLenLen := writeVarint(nameLen[:], len(n)) + tagLenLen := writeVarint(tagLen[:], len(tag)) + + var bits byte + l := 1 + nameLenLen + len(n) + if exported { + bits |= 1 << 0 + } + if len(tag) > 0 { + l += tagLenLen + len(tag) + bits |= 1 << 1 + } + if embedded { + bits |= 1 << 3 + } + + b := make([]byte, l) + b[0] = bits + copy(b[1:], nameLen[:nameLenLen]) + copy(b[1+nameLenLen:], n) + if len(tag) > 0 { + tb := b[1+nameLenLen+len(n):] + copy(tb, tagLen[:tagLenLen]) + copy(tb[tagLenLen:], tag) + } + + return Name{Bytes: &b[0]} +} + +const ( + TraceArgsLimit = 10 // print no more than 10 args/components + TraceArgsMaxDepth = 5 // no more than 5 layers of nesting + + // maxLen is a (conservative) upper bound of the byte stream length. For + // each arg/component, it has no more than 2 bytes of data (size, offset), + // and no more than one {, }, ... at each level (it cannot have both the + // data and ... unless it is the last one, just be conservative). Plus 1 + // for _endSeq. + TraceArgsMaxLen = (TraceArgsMaxDepth*3+2)*TraceArgsLimit + 1 +) + +// Populate the data. +// The data is a stream of bytes, which contains the offsets and sizes of the +// non-aggregate arguments or non-aggregate fields/elements of aggregate-typed +// arguments, along with special "operators". Specifically, +// - for each non-aggregate arg/field/element, its offset from FP (1 byte) and +// size (1 byte) +// - special operators: +// - 0xff - end of sequence +// - 0xfe - print { (at the start of an aggregate-typed argument) +// - 0xfd - print } (at the end of an aggregate-typed argument) +// - 0xfc - print ... (more args/fields/elements) +// - 0xfb - print _ (offset too large) +const ( + TraceArgsEndSeq = 0xff + TraceArgsStartAgg = 0xfe + TraceArgsEndAgg = 0xfd + TraceArgsDotdotdot = 0xfc + TraceArgsOffsetTooLarge = 0xfb + TraceArgsSpecial = 0xf0 // above this are operators, below this are ordinary offsets +) + +// MaxPtrmaskBytes is the maximum length of a GC ptrmask bitmap, +// which holds 1-bit entries describing where pointers are in a given type. +// Above this length, the GC information is recorded as a GC program, +// which can express repetition compactly. In either form, the +// information is used by the runtime to initialize the heap bitmap, +// and for large types (like 128 or more words), they are roughly the +// same speed. GC programs are never much larger and often more +// compact. (If large arrays are involved, they can be arbitrarily +// more compact.) +// +// The cutoff must be large enough that any allocation large enough to +// use a GC program is large enough that it does not share heap bitmap +// bytes with any other objects, allowing the GC program execution to +// assume an aligned start and not use atomic operations. In the current +// runtime, this means all malloc size classes larger than the cutoff must +// be multiples of four words. On 32-bit systems that's 16 bytes, and +// all size classes >= 16 bytes are 16-byte aligned, so no real constraint. +// On 64-bit systems, that's 32 bytes, and 32-byte alignment is guaranteed +// for size classes >= 256 bytes. On a 64-bit system, 256 bytes allocated +// is 32 pointers, the bits for which fit in 4 bytes. So MaxPtrmaskBytes +// must be >= 4. +// +// We used to use 16 because the GC programs do have some constant overhead +// to get started, and processing 128 pointers seems to be enough to +// amortize that overhead well. +// +// To make sure that the runtime's chansend can call typeBitsBulkBarrier, +// we raised the limit to 2048, so that even 32-bit systems are guaranteed to +// use bitmaps for objects up to 64 kB in size. +const MaxPtrmaskBytes = 2048 diff --git a/internal/go/src/archive/archive.go b/internal/go/src/archive/archive.go new file mode 100755 index 0000000..8623acb --- /dev/null +++ b/internal/go/src/archive/archive.go @@ -0,0 +1,503 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package archive implements reading of archive files generated by the Go +// toolchain. +package archive + +import ( + "bufio" + "bytes" + "loov.dev/lensm/internal/go/src/bio" + "loov.dev/lensm/internal/go/src/goobj" + "errors" + "fmt" + "io" + "log" + "os" + "strconv" + "strings" + "time" + "unicode/utf8" +) + +/* +The archive format is: + +First, on a line by itself + ! + +Then zero or more file records. Each file record has a fixed-size one-line header +followed by data bytes followed by an optional padding byte. The header is: + + %-16s%-12d%-6d%-6d%-8o%-10d` + name mtime uid gid mode size + +(note the trailing backquote). The %-16s here means at most 16 *bytes* of +the name, and if shorter, space padded on the right. +*/ + +// A Data is a reference to data stored in an object file. +// It records the offset and size of the data, so that a client can +// read the data only if necessary. +type Data struct { + Offset int64 + Size int64 +} + +type Archive struct { + f *os.File + Entries []Entry +} + +func (a *Archive) File() *os.File { return a.f } + +type Entry struct { + Name string + Type EntryType + Mtime int64 + Uid int + Gid int + Mode os.FileMode + Data + Obj *GoObj // nil if this entry is not a Go object file +} + +type EntryType int + +const ( + EntryPkgDef EntryType = iota + EntryGoObj + EntryNativeObj + EntrySentinelNonObj +) + +func (e *Entry) String() string { + return fmt.Sprintf("%s %6d/%-6d %12d %s %s", + (e.Mode & 0777).String(), + e.Uid, + e.Gid, + e.Size, + time.Unix(e.Mtime, 0).Format(timeFormat), + e.Name) +} + +type GoObj struct { + TextHeader []byte + Arch string + Data +} + +const ( + entryHeader = "%s%-12d%-6d%-6d%-8o%-10d`\n" + // In entryHeader the first entry, the name, is always printed as 16 bytes right-padded. + entryLen = 16 + 12 + 6 + 6 + 8 + 10 + 1 + 1 + timeFormat = "Jan _2 15:04 2006" +) + +var ( + archiveHeader = []byte("!\n") + archiveMagic = []byte("`\n") + goobjHeader = []byte("go objec") // truncated to size of archiveHeader + + errCorruptArchive = errors.New("corrupt archive") + errTruncatedArchive = errors.New("truncated archive") + errCorruptObject = errors.New("corrupt object file") + errNotObject = errors.New("unrecognized object file format") +) + +type ErrGoObjOtherVersion struct{ magic []byte } + +func (e ErrGoObjOtherVersion) Error() string { + return fmt.Sprintf("go object of a different version: %q", e.magic) +} + +// An objReader is an object file reader. +type objReader struct { + a *Archive + b *bio.Reader + err error + offset int64 + limit int64 + tmp [256]byte +} + +func (r *objReader) init(f *os.File) { + r.a = &Archive{f, nil} + r.offset, _ = f.Seek(0, io.SeekCurrent) + r.limit, _ = f.Seek(0, io.SeekEnd) + f.Seek(r.offset, io.SeekStart) + r.b = bio.NewReader(f) +} + +// error records that an error occurred. +// It returns only the first error, so that an error +// caused by an earlier error does not discard information +// about the earlier error. +func (r *objReader) error(err error) error { + if r.err == nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + r.err = err + } + // panic("corrupt") // useful for debugging + return r.err +} + +// peek returns the next n bytes without advancing the reader. +func (r *objReader) peek(n int) ([]byte, error) { + if r.err != nil { + return nil, r.err + } + if r.offset >= r.limit { + r.error(io.ErrUnexpectedEOF) + return nil, r.err + } + b, err := r.b.Peek(n) + if err != nil { + if err != bufio.ErrBufferFull { + r.error(err) + } + } + return b, err +} + +// readByte reads and returns a byte from the input file. +// On I/O error or EOF, it records the error but returns byte 0. +// A sequence of 0 bytes will eventually terminate any +// parsing state in the object file. In particular, it ends the +// reading of a varint. +func (r *objReader) readByte() byte { + if r.err != nil { + return 0 + } + if r.offset >= r.limit { + r.error(io.ErrUnexpectedEOF) + return 0 + } + b, err := r.b.ReadByte() + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + r.error(err) + b = 0 + } else { + r.offset++ + } + return b +} + +// readFull reads exactly len(b) bytes from the input file. +// If an error occurs, read returns the error but also +// records it, so it is safe for callers to ignore the result +// as long as delaying the report is not a problem. +func (r *objReader) readFull(b []byte) error { + if r.err != nil { + return r.err + } + if r.offset+int64(len(b)) > r.limit { + return r.error(io.ErrUnexpectedEOF) + } + n, err := io.ReadFull(r.b, b) + r.offset += int64(n) + if err != nil { + return r.error(err) + } + return nil +} + +// skip skips n bytes in the input. +func (r *objReader) skip(n int64) { + if n < 0 { + r.error(fmt.Errorf("debug/goobj: internal error: misuse of skip")) + } + if n < int64(len(r.tmp)) { + // Since the data is so small, a just reading from the buffered + // reader is better than flushing the buffer and seeking. + r.readFull(r.tmp[:n]) + } else if n <= int64(r.b.Buffered()) { + // Even though the data is not small, it has already been read. + // Advance the buffer instead of seeking. + for n > int64(len(r.tmp)) { + r.readFull(r.tmp[:]) + n -= int64(len(r.tmp)) + } + r.readFull(r.tmp[:n]) + } else { + // Seek, giving up buffered data. + r.b.MustSeek(r.offset+n, io.SeekStart) + r.offset += n + } +} + +// New writes to f to make a new archive. +func New(f *os.File) (*Archive, error) { + _, err := f.Write(archiveHeader) + if err != nil { + return nil, err + } + return &Archive{f: f}, nil +} + +// Parse parses an object file or archive from f. +func Parse(f *os.File, verbose bool) (*Archive, error) { + var r objReader + r.init(f) + t, err := r.peek(8) + if err != nil { + if err == io.EOF { + err = io.ErrUnexpectedEOF + } + return nil, err + } + + switch { + default: + return nil, errNotObject + + case bytes.Equal(t, archiveHeader): + if err := r.parseArchive(verbose); err != nil { + return nil, err + } + case bytes.Equal(t, goobjHeader): + off := r.offset + o := &GoObj{} + if err := r.parseObject(o, r.limit-off); err != nil { + return nil, err + } + r.a.Entries = []Entry{{ + Name: f.Name(), + Type: EntryGoObj, + Data: Data{off, r.limit - off}, + Obj: o, + }} + } + + return r.a, nil +} + +// trimSpace removes trailing spaces from b and returns the corresponding string. +// This effectively parses the form used in archive headers. +func trimSpace(b []byte) string { + return string(bytes.TrimRight(b, " ")) +} + +// parseArchive parses a Unix archive of Go object files. +func (r *objReader) parseArchive(verbose bool) error { + r.readFull(r.tmp[:8]) // consume header (already checked) + for r.offset < r.limit { + if err := r.readFull(r.tmp[:60]); err != nil { + return err + } + data := r.tmp[:60] + + // Each file is preceded by this text header (slice indices in first column): + // 0:16 name + // 16:28 date + // 28:34 uid + // 34:40 gid + // 40:48 mode + // 48:58 size + // 58:60 magic - `\n + // We only care about name, size, and magic, unless in verbose mode. + // The fields are space-padded on the right. + // The size is in decimal. + // The file data - size bytes - follows the header. + // Headers are 2-byte aligned, so if size is odd, an extra padding + // byte sits between the file data and the next header. + // The file data that follows is padded to an even number of bytes: + // if size is odd, an extra padding byte is inserted betw the next header. + if len(data) < 60 { + return errTruncatedArchive + } + if !bytes.Equal(data[58:60], archiveMagic) { + return errCorruptArchive + } + name := trimSpace(data[0:16]) + var err error + get := func(start, end, base, bitsize int) int64 { + if err != nil { + return 0 + } + var v int64 + v, err = strconv.ParseInt(trimSpace(data[start:end]), base, bitsize) + return v + } + size := get(48, 58, 10, 64) + var ( + mtime int64 + uid, gid int + mode os.FileMode + ) + if verbose { + mtime = get(16, 28, 10, 64) + uid = int(get(28, 34, 10, 32)) + gid = int(get(34, 40, 10, 32)) + mode = os.FileMode(get(40, 48, 8, 32)) + } + if err != nil { + return errCorruptArchive + } + data = data[60:] + fsize := size + size&1 + if fsize < 0 || fsize < size { + return errCorruptArchive + } + switch name { + case "__.PKGDEF": + r.a.Entries = append(r.a.Entries, Entry{ + Name: name, + Type: EntryPkgDef, + Mtime: mtime, + Uid: uid, + Gid: gid, + Mode: mode, + Data: Data{r.offset, size}, + }) + r.skip(size) + case "preferlinkext", "dynimportfail": + if size == 0 { + // These are not actual objects, but rather sentinel + // entries put into the archive by the Go command to + // be read by the linker. See #62036. + r.a.Entries = append(r.a.Entries, Entry{ + Name: name, + Type: EntrySentinelNonObj, + Mtime: mtime, + Uid: uid, + Gid: gid, + Mode: mode, + Data: Data{r.offset, size}, + }) + break + } + fallthrough + default: + var typ EntryType + var o *GoObj + offset := r.offset + p, err := r.peek(8) + if err != nil { + return err + } + if bytes.Equal(p, goobjHeader) { + typ = EntryGoObj + o = &GoObj{} + err := r.parseObject(o, size) + if err != nil { + return err + } + } else { + typ = EntryNativeObj + r.skip(size) + } + r.a.Entries = append(r.a.Entries, Entry{ + Name: name, + Type: typ, + Mtime: mtime, + Uid: uid, + Gid: gid, + Mode: mode, + Data: Data{offset, size}, + Obj: o, + }) + } + if size&1 != 0 { + r.skip(1) + } + } + return nil +} + +// parseObject parses a single Go object file. +// The object file consists of a textual header ending in "\n!\n" +// and then the part we want to parse begins. +// The format of that part is defined in a comment at the top +// of cmd/internal/goobj/objfile.go. +func (r *objReader) parseObject(o *GoObj, size int64) error { + h := make([]byte, 0, 256) + var c1, c2, c3 byte + for { + c1, c2, c3 = c2, c3, r.readByte() + h = append(h, c3) + // The new export format can contain 0 bytes. + // Don't consider them errors, only look for r.err != nil. + if r.err != nil { + return errCorruptObject + } + if c1 == '\n' && c2 == '!' && c3 == '\n' { + break + } + } + o.TextHeader = h + hs := strings.Fields(string(h)) + if len(hs) >= 4 { + o.Arch = hs[3] + } + o.Offset = r.offset + o.Size = size - int64(len(h)) + + p, err := r.peek(8) + if err != nil { + return err + } + if !bytes.Equal(p, []byte(goobj.Magic)) { + if bytes.HasPrefix(p, []byte("\x00go1")) && bytes.HasSuffix(p, []byte("ld")) { + return r.error(ErrGoObjOtherVersion{p[1:]}) // strip the \x00 byte + } + return r.error(errCorruptObject) + } + r.skip(o.Size) + return nil +} + +// AddEntry adds an entry to the end of a, with the content from r. +func (a *Archive) AddEntry(typ EntryType, name string, mtime int64, uid, gid int, mode os.FileMode, size int64, r io.Reader) { + off, err := a.f.Seek(0, io.SeekEnd) + if err != nil { + log.Fatal(err) + } + n, err := fmt.Fprintf(a.f, entryHeader, exactly16Bytes(name), mtime, uid, gid, mode, size) + if err != nil || n != entryLen { + log.Fatal("writing entry header: ", err) + } + n1, _ := io.CopyN(a.f, r, size) + if n1 != size { + log.Fatal(err) + } + if (off+size)&1 != 0 { + a.f.Write([]byte{0}) // pad to even byte + } + a.Entries = append(a.Entries, Entry{ + Name: name, + Type: typ, + Mtime: mtime, + Uid: uid, + Gid: gid, + Mode: mode, + Data: Data{off + entryLen, size}, + }) +} + +// exactly16Bytes truncates the string if necessary so it is at most 16 bytes long, +// then pads the result with spaces to be exactly 16 bytes. +// Fmt uses runes for its width calculation, but we need bytes in the entry header. +func exactly16Bytes(s string) string { + for len(s) > 16 { + _, wid := utf8.DecodeLastRuneInString(s) + s = s[:len(s)-wid] + } + const sixteenSpaces = " " + s += sixteenSpaces[:16-len(s)] + return s +} + +// architecture-independent object file output +const HeaderSize = 60 + +func FormatHeader(arhdr []byte, name string, size int64) { + copy(arhdr[:], fmt.Sprintf("%-16s%-12d%-6d%-6d%-8o%-10d`\n", name, 0, 0, 0, 0644, size)) +} diff --git a/internal/go/src/bio/buf.go b/internal/go/src/bio/buf.go new file mode 100755 index 0000000..c4c2514 --- /dev/null +++ b/internal/go/src/bio/buf.go @@ -0,0 +1,148 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package bio implements common I/O abstractions used within the Go toolchain. +package bio + +import ( + "bufio" + "io" + "log" + "os" +) + +// Reader implements a seekable buffered io.Reader. +type Reader struct { + f *os.File + *bufio.Reader +} + +// Writer implements a seekable buffered io.Writer. +type Writer struct { + f *os.File + *bufio.Writer +} + +// Create creates the file named name and returns a Writer +// for that file. +func Create(name string) (*Writer, error) { + f, err := os.Create(name) + if err != nil { + return nil, err + } + return &Writer{f: f, Writer: bufio.NewWriter(f)}, nil +} + +// Open returns a Reader for the file named name. +func Open(name string) (*Reader, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + return NewReader(f), nil +} + +// NewReader returns a Reader from an open file. +func NewReader(f *os.File) *Reader { + return &Reader{f: f, Reader: bufio.NewReader(f)} +} + +func (r *Reader) MustSeek(offset int64, whence int) int64 { + if whence == 1 { + offset -= int64(r.Buffered()) + } + off, err := r.f.Seek(offset, whence) + if err != nil { + log.Fatalf("seeking in output: %v", err) + } + r.Reset(r.f) + return off +} + +func (w *Writer) MustSeek(offset int64, whence int) int64 { + if err := w.Flush(); err != nil { + log.Fatalf("writing output: %v", err) + } + off, err := w.f.Seek(offset, whence) + if err != nil { + log.Fatalf("seeking in output: %v", err) + } + return off +} + +func (r *Reader) Offset() int64 { + off, err := r.f.Seek(0, 1) + if err != nil { + log.Fatalf("seeking in output [0, 1]: %v", err) + } + off -= int64(r.Buffered()) + return off +} + +func (w *Writer) Offset() int64 { + if err := w.Flush(); err != nil { + log.Fatalf("writing output: %v", err) + } + off, err := w.f.Seek(0, 1) + if err != nil { + log.Fatalf("seeking in output [0, 1]: %v", err) + } + return off +} + +func (r *Reader) Close() error { + return r.f.Close() +} + +func (w *Writer) Close() error { + err := w.Flush() + err1 := w.f.Close() + if err == nil { + err = err1 + } + return err +} + +func (r *Reader) File() *os.File { + return r.f +} + +func (w *Writer) File() *os.File { + return w.f +} + +// Slice reads the next length bytes of r into a slice. +// +// This slice may be backed by mmap'ed memory. Currently, this memory +// will never be unmapped. The second result reports whether the +// backing memory is read-only. +func (r *Reader) Slice(length uint64) ([]byte, bool, error) { + if length == 0 { + return []byte{}, false, nil + } + + data, ok := r.sliceOS(length) + if ok { + return data, true, nil + } + + data = make([]byte, length) + _, err := io.ReadFull(r, data) + if err != nil { + return nil, false, err + } + return data, false, nil +} + +// SliceRO returns a slice containing the next length bytes of r +// backed by a read-only mmap'd data. If the mmap cannot be +// established (limit exceeded, region too small, etc) a nil slice +// will be returned. If mmap succeeds, it will never be unmapped. +func (r *Reader) SliceRO(length uint64) []byte { + data, ok := r.sliceOS(length) + if ok { + return data + } + return nil +} diff --git a/internal/go/src/bio/buf_mmap.go b/internal/go/src/bio/buf_mmap.go new file mode 100755 index 0000000..9ce2b4d --- /dev/null +++ b/internal/go/src/bio/buf_mmap.go @@ -0,0 +1,62 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build unix + +package bio + +import ( + "runtime" + "sync/atomic" + "syscall" +) + +// mmapLimit is the maximum number of mmaped regions to create before +// falling back to reading into a heap-allocated slice. This exists +// because some operating systems place a limit on the number of +// distinct mapped regions per process. As of this writing: +// +// Darwin unlimited +// DragonFly 1000000 (vm.max_proc_mmap) +// FreeBSD unlimited +// Linux 65530 (vm.max_map_count) // TODO: query /proc/sys/vm/max_map_count? +// NetBSD unlimited +// OpenBSD unlimited +var mmapLimit int32 = 1<<31 - 1 + +func init() { + // Linux is the only practically concerning OS. + if runtime.GOOS == "linux" { + mmapLimit = 30000 + } +} + +func (r *Reader) sliceOS(length uint64) ([]byte, bool) { + // For small slices, don't bother with the overhead of a + // mapping, especially since we have no way to unmap it. + const threshold = 16 << 10 + if length < threshold { + return nil, false + } + + // Have we reached the mmap limit? + if atomic.AddInt32(&mmapLimit, -1) < 0 { + atomic.AddInt32(&mmapLimit, 1) + return nil, false + } + + // Page-align the offset. + off := r.Offset() + align := syscall.Getpagesize() + aoff := off &^ int64(align-1) + + data, err := syscall.Mmap(int(r.f.Fd()), aoff, int(length+uint64(off-aoff)), syscall.PROT_READ, syscall.MAP_SHARED|syscall.MAP_FILE) + if err != nil { + return nil, false + } + + data = data[off-aoff:] + r.MustSeek(int64(length), 1) + return data, true +} diff --git a/internal/go/src/bio/buf_nommap.go b/internal/go/src/bio/buf_nommap.go new file mode 100755 index 0000000..1d78a01 --- /dev/null +++ b/internal/go/src/bio/buf_nommap.go @@ -0,0 +1,11 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build !unix + +package bio + +func (r *Reader) sliceOS(length uint64) ([]byte, bool) { + return nil, false +} diff --git a/internal/go/src/bisect/bisect.go b/internal/go/src/bisect/bisect.go new file mode 100755 index 0000000..a79bb80 --- /dev/null +++ b/internal/go/src/bisect/bisect.go @@ -0,0 +1,778 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package bisect can be used by compilers and other programs +// to serve as a target for the bisect debugging tool. +// See [golang.org/x/tools/cmd/bisect] for details about using the tool. +// +// To be a bisect target, allowing bisect to help determine which of a set of independent +// changes provokes a failure, a program needs to: +// +// 1. Define a way to accept a change pattern on its command line or in its environment. +// The most common mechanism is a command-line flag. +// The pattern can be passed to [New] to create a [Matcher], the compiled form of a pattern. +// +// 2. Assign each change a unique ID. One possibility is to use a sequence number, +// but the most common mechanism is to hash some kind of identifying information +// like the file and line number where the change might be applied. +// [Hash] hashes its arguments to compute an ID. +// +// 3. Enable each change that the pattern says should be enabled. +// The [Matcher.ShouldEnable] method answers this question for a given change ID. +// +// 4. Print a report identifying each change that the pattern says should be printed. +// The [Matcher.ShouldPrint] method answers this question for a given change ID. +// The report consists of one more lines on standard error or standard output +// that contain a “match marker”. [Marker] returns the match marker for a given ID. +// When bisect reports a change as causing the failure, it identifies the change +// by printing the report lines with the match marker removed. +// +// # Example Usage +// +// A program starts by defining how it receives the pattern. In this example, we will assume a flag. +// The next step is to compile the pattern: +// +// m, err := bisect.New(patternFlag) +// if err != nil { +// log.Fatal(err) +// } +// +// Then, each time a potential change is considered, the program computes +// a change ID by hashing identifying information (source file and line, in this case) +// and then calls m.ShouldPrint and m.ShouldEnable to decide whether to +// print and enable the change, respectively. The two can return different values +// depending on whether bisect is trying to find a minimal set of changes to +// disable or to enable to provoke the failure. +// +// It is usually helpful to write a helper function that accepts the identifying information +// and then takes care of hashing, printing, and reporting whether the identified change +// should be enabled. For example, a helper for changes identified by a file and line number +// would be: +// +// func ShouldEnable(file string, line int) { +// h := bisect.Hash(file, line) +// if m.ShouldPrint(h) { +// fmt.Fprintf(os.Stderr, "%v %s:%d\n", bisect.Marker(h), file, line) +// } +// return m.ShouldEnable(h) +// } +// +// Finally, note that New returns a nil Matcher when there is no pattern, +// meaning that the target is not running under bisect at all, +// so all changes should be enabled and none should be printed. +// In that common case, the computation of the hash can be avoided entirely +// by checking for m == nil first: +// +// func ShouldEnable(file string, line int) bool { +// if m == nil { +// return true +// } +// h := bisect.Hash(file, line) +// if m.ShouldPrint(h) { +// fmt.Fprintf(os.Stderr, "%v %s:%d\n", bisect.Marker(h), file, line) +// } +// return m.ShouldEnable(h) +// } +// +// When the identifying information is expensive to format, this code can call +// [Matcher.MarkerOnly] to find out whether short report lines containing only the +// marker are permitted for a given run. (Bisect permits such lines when it is +// still exploring the space of possible changes and will not be showing the +// output to the user.) If so, the client can choose to print only the marker: +// +// func ShouldEnable(file string, line int) bool { +// if m == nil { +// return true +// } +// h := bisect.Hash(file, line) +// if m.ShouldPrint(h) { +// if m.MarkerOnly() { +// bisect.PrintMarker(os.Stderr, h) +// } else { +// fmt.Fprintf(os.Stderr, "%v %s:%d\n", bisect.Marker(h), file, line) +// } +// } +// return m.ShouldEnable(h) +// } +// +// This specific helper – deciding whether to enable a change identified by +// file and line number and printing about the change when necessary – is +// provided by the [Matcher.FileLine] method. +// +// Another common usage is deciding whether to make a change in a function +// based on the caller's stack, to identify the specific calling contexts that the +// change breaks. The [Matcher.Stack] method takes care of obtaining the stack, +// printing it when necessary, and reporting whether to enable the change +// based on that stack. +// +// # Pattern Syntax +// +// Patterns are generated by the bisect tool and interpreted by [New]. +// Users should not have to understand the patterns except when +// debugging a target's bisect support or debugging the bisect tool itself. +// +// The pattern syntax selecting a change is a sequence of bit strings +// separated by + and - operators. Each bit string denotes the set of +// changes with IDs ending in those bits, + is set addition, - is set subtraction, +// and the expression is evaluated in the usual left-to-right order. +// The special binary number “y” denotes the set of all changes, +// standing in for the empty bit string. +// In the expression, all the + operators must appear before all the - operators. +// A leading + adds to an empty set. A leading - subtracts from the set of all +// possible suffixes. +// +// For example: +// +// - “01+10” and “+01+10” both denote the set of changes +// with IDs ending with the bits 01 or 10. +// +// - “01+10-1001” denotes the set of changes with IDs +// ending with the bits 01 or 10, but excluding those ending in 1001. +// +// - “-01-1000” and “y-01-1000 both denote the set of all changes +// with IDs not ending in 01 nor 1000. +// +// - “0+1-01+001” is not a valid pattern, because all the + operators do not +// appear before all the - operators. +// +// In the syntaxes described so far, the pattern specifies the changes to +// enable and report. If a pattern is prefixed by a “!”, the meaning +// changes: the pattern specifies the changes to DISABLE and report. This +// mode of operation is needed when a program passes with all changes +// enabled but fails with no changes enabled. In this case, bisect +// searches for minimal sets of changes to disable. +// Put another way, the leading “!” inverts the result from [Matcher.ShouldEnable] +// but does not invert the result from [Matcher.ShouldPrint]. +// +// As a convenience for manual debugging, “n” is an alias for “!y”, +// meaning to disable and report all changes. +// +// Finally, a leading “v” in the pattern indicates that the reports will be shown +// to the user of bisect to describe the changes involved in a failure. +// At the API level, the leading “v” causes [Matcher.Visible] to return true. +// See the next section for details. +// +// # Match Reports +// +// The target program must enable only those changed matched +// by the pattern, and it must print a match report for each such change. +// A match report consists of one or more lines of text that will be +// printed by the bisect tool to describe a change implicated in causing +// a failure. Each line in the report for a given change must contain a +// match marker with that change ID, as returned by [Marker]. +// The markers are elided when displaying the lines to the user. +// +// A match marker has the form “[bisect-match 0x1234]” where +// 0x1234 is the change ID in hexadecimal. +// An alternate form is “[bisect-match 010101]”, giving the change ID in binary. +// +// When [Matcher.Visible] returns false, the match reports are only +// being processed by bisect to learn the set of enabled changes, +// not shown to the user, meaning that each report can be a match +// marker on a line by itself, eliding the usual textual description. +// When the textual description is expensive to compute, +// checking [Matcher.Visible] can help the avoid that expense +// in most runs. +package bisect + +import ( + "runtime" + "sync" + "sync/atomic" +) + +// New creates and returns a new Matcher implementing the given pattern. +// The pattern syntax is defined in the package doc comment. +// +// In addition to the pattern syntax syntax, New("") returns nil, nil. +// The nil *Matcher is valid for use: it returns true from ShouldEnable +// and false from ShouldPrint for all changes. Callers can avoid calling +// [Hash], [Matcher.ShouldEnable], and [Matcher.ShouldPrint] entirely +// when they recognize the nil Matcher. +func New(pattern string) (*Matcher, error) { + if pattern == "" { + return nil, nil + } + + m := new(Matcher) + + p := pattern + // Special case for leading 'q' so that 'qn' quietly disables, e.g. fmahash=qn to disable fma + // Any instance of 'v' disables 'q'. + if len(p) > 0 && p[0] == 'q' { + m.quiet = true + p = p[1:] + if p == "" { + return nil, &parseError{"invalid pattern syntax: " + pattern} + } + } + // Allow multiple v, so that “bisect cmd vPATTERN” can force verbose all the time. + for len(p) > 0 && p[0] == 'v' { + m.verbose = true + m.quiet = false + p = p[1:] + if p == "" { + return nil, &parseError{"invalid pattern syntax: " + pattern} + } + } + + // Allow multiple !, each negating the last, so that “bisect cmd !PATTERN” works + // even when bisect chooses to add its own !. + m.enable = true + for len(p) > 0 && p[0] == '!' { + m.enable = !m.enable + p = p[1:] + if p == "" { + return nil, &parseError{"invalid pattern syntax: " + pattern} + } + } + + if p == "n" { + // n is an alias for !y. + m.enable = !m.enable + p = "y" + } + + // Parse actual pattern syntax. + result := true + bits := uint64(0) + start := 0 + wid := 1 // 1-bit (binary); sometimes 4-bit (hex) + for i := 0; i <= len(p); i++ { + // Imagine a trailing - at the end of the pattern to flush final suffix + c := byte('-') + if i < len(p) { + c = p[i] + } + if i == start && wid == 1 && c == 'x' { // leading x for hex + start = i + 1 + wid = 4 + continue + } + switch c { + default: + return nil, &parseError{"invalid pattern syntax: " + pattern} + case '2', '3', '4', '5', '6', '7', '8', '9': + if wid != 4 { + return nil, &parseError{"invalid pattern syntax: " + pattern} + } + fallthrough + case '0', '1': + bits <<= wid + bits |= uint64(c - '0') + case 'a', 'b', 'c', 'd', 'e', 'f', 'A', 'B', 'C', 'D', 'E', 'F': + if wid != 4 { + return nil, &parseError{"invalid pattern syntax: " + pattern} + } + bits <<= 4 + bits |= uint64(c&^0x20 - 'A' + 10) + case 'y': + if i+1 < len(p) && (p[i+1] == '0' || p[i+1] == '1') { + return nil, &parseError{"invalid pattern syntax: " + pattern} + } + bits = 0 + case '+', '-': + if c == '+' && result == false { + // Have already seen a -. Should be - from here on. + return nil, &parseError{"invalid pattern syntax (+ after -): " + pattern} + } + if i > 0 { + n := (i - start) * wid + if n > 64 { + return nil, &parseError{"pattern bits too long: " + pattern} + } + if n <= 0 { + return nil, &parseError{"invalid pattern syntax: " + pattern} + } + if p[start] == 'y' { + n = 0 + } + mask := uint64(1)<= 0; i-- { + c := &m.list[i] + if id&c.mask == c.bits { + return c.result + } + } + return false +} + +// FileLine reports whether the change identified by file and line should be enabled. +// If the change should be printed, FileLine prints a one-line report to w. +func (m *Matcher) FileLine(w Writer, file string, line int) bool { + if m == nil { + return true + } + return m.fileLine(w, file, line) +} + +// fileLine does the real work for FileLine. +// This lets FileLine's body handle m == nil and potentially be inlined. +func (m *Matcher) fileLine(w Writer, file string, line int) bool { + h := Hash(file, line) + if m.ShouldPrint(h) { + if m.MarkerOnly() { + PrintMarker(w, h) + } else { + printFileLine(w, h, file, line) + } + } + return m.ShouldEnable(h) +} + +// printFileLine prints a non-marker-only report for file:line to w. +func printFileLine(w Writer, h uint64, file string, line int) error { + const markerLen = 40 // overestimate + b := make([]byte, 0, markerLen+len(file)+24) + b = AppendMarker(b, h) + b = appendFileLine(b, file, line) + b = append(b, '\n') + _, err := w.Write(b) + return err +} + +// appendFileLine appends file:line to dst, returning the extended slice. +func appendFileLine(dst []byte, file string, line int) []byte { + dst = append(dst, file...) + dst = append(dst, ':') + u := uint(line) + if line < 0 { + dst = append(dst, '-') + u = -u + } + var buf [24]byte + i := len(buf) + for i == len(buf) || u > 0 { + i-- + buf[i] = '0' + byte(u%10) + u /= 10 + } + dst = append(dst, buf[i:]...) + return dst +} + +// MatchStack assigns the current call stack a change ID. +// If the stack should be printed, MatchStack prints it. +// Then MatchStack reports whether a change at the current call stack should be enabled. +func (m *Matcher) Stack(w Writer) bool { + if m == nil { + return true + } + return m.stack(w) +} + +// stack does the real work for Stack. +// This lets stack's body handle m == nil and potentially be inlined. +func (m *Matcher) stack(w Writer) bool { + const maxStack = 16 + var stk [maxStack]uintptr + n := runtime.Callers(2, stk[:]) + // caller #2 is not for printing; need it to normalize PCs if ASLR. + if n <= 1 { + return false + } + + base := stk[0] + // normalize PCs + for i := range stk[:n] { + stk[i] -= base + } + + h := Hash(stk[:n]) + if m.ShouldPrint(h) { + var d *dedup + for { + d = m.dedup.Load() + if d != nil { + break + } + d = new(dedup) + if m.dedup.CompareAndSwap(nil, d) { + break + } + } + + if m.MarkerOnly() { + if !d.seenLossy(h) { + PrintMarker(w, h) + } + } else { + if !d.seen(h) { + // Restore PCs in stack for printing + for i := range stk[:n] { + stk[i] += base + } + printStack(w, h, stk[1:n]) + } + } + } + return m.ShouldEnable(h) +} + +// Writer is the same interface as io.Writer. +// It is duplicated here to avoid importing io. +type Writer interface { + Write([]byte) (int, error) +} + +// PrintMarker prints to w a one-line report containing only the marker for h. +// It is appropriate to use when [Matcher.ShouldPrint] and [Matcher.MarkerOnly] both return true. +func PrintMarker(w Writer, h uint64) error { + var buf [50]byte + b := AppendMarker(buf[:0], h) + b = append(b, '\n') + _, err := w.Write(b) + return err +} + +// printStack prints to w a multi-line report containing a formatting of the call stack stk, +// with each line preceded by the marker for h. +func printStack(w Writer, h uint64, stk []uintptr) error { + buf := make([]byte, 0, 2048) + + var prefixBuf [100]byte + prefix := AppendMarker(prefixBuf[:0], h) + + frames := runtime.CallersFrames(stk) + for { + f, more := frames.Next() + buf = append(buf, prefix...) + buf = append(buf, f.Function...) + buf = append(buf, "()\n"...) + buf = append(buf, prefix...) + buf = append(buf, '\t') + buf = appendFileLine(buf, f.File, f.Line) + buf = append(buf, '\n') + if !more { + break + } + } + buf = append(buf, prefix...) + buf = append(buf, '\n') + _, err := w.Write(buf) + return err +} + +// Marker returns the match marker text to use on any line reporting details +// about a match of the given ID. +// It always returns the hexadecimal format. +func Marker(id uint64) string { + return string(AppendMarker(nil, id)) +} + +// AppendMarker is like [Marker] but appends the marker to dst. +func AppendMarker(dst []byte, id uint64) []byte { + const prefix = "[bisect-match 0x" + var buf [len(prefix) + 16 + 1]byte + copy(buf[:], prefix) + for i := 0; i < 16; i++ { + buf[len(prefix)+i] = "0123456789abcdef"[id>>60] + id <<= 4 + } + buf[len(prefix)+16] = ']' + return append(dst, buf[:]...) +} + +// CutMarker finds the first match marker in line and removes it, +// returning the shortened line (with the marker removed), +// the ID from the match marker, +// and whether a marker was found at all. +// If there is no marker, CutMarker returns line, 0, false. +func CutMarker(line string) (short string, id uint64, ok bool) { + // Find first instance of prefix. + prefix := "[bisect-match " + i := 0 + for ; ; i++ { + if i >= len(line)-len(prefix) { + return line, 0, false + } + if line[i] == '[' && line[i:i+len(prefix)] == prefix { + break + } + } + + // Scan to ]. + j := i + len(prefix) + for j < len(line) && line[j] != ']' { + j++ + } + if j >= len(line) { + return line, 0, false + } + + // Parse id. + idstr := line[i+len(prefix) : j] + if len(idstr) >= 3 && idstr[:2] == "0x" { + // parse hex + if len(idstr) > 2+16 { // max 0x + 16 digits + return line, 0, false + } + for i := 2; i < len(idstr); i++ { + id <<= 4 + switch c := idstr[i]; { + case '0' <= c && c <= '9': + id |= uint64(c - '0') + case 'a' <= c && c <= 'f': + id |= uint64(c - 'a' + 10) + case 'A' <= c && c <= 'F': + id |= uint64(c - 'A' + 10) + } + } + } else { + if idstr == "" || len(idstr) > 64 { // min 1 digit, max 64 digits + return line, 0, false + } + // parse binary + for i := 0; i < len(idstr); i++ { + id <<= 1 + switch c := idstr[i]; c { + default: + return line, 0, false + case '0', '1': + id |= uint64(c - '0') + } + } + } + + // Construct shortened line. + // Remove at most one space from around the marker, + // so that "foo [marker] bar" shortens to "foo bar". + j++ // skip ] + if i > 0 && line[i-1] == ' ' { + i-- + } else if j < len(line) && line[j] == ' ' { + j++ + } + short = line[:i] + line[j:] + return short, id, true +} + +// Hash computes a hash of the data arguments, +// each of which must be of type string, byte, int, uint, int32, uint32, int64, uint64, uintptr, or a slice of one of those types. +func Hash(data ...any) uint64 { + h := offset64 + for _, v := range data { + switch v := v.(type) { + default: + // Note: Not printing the type, because reflect.ValueOf(v) + // would make the interfaces prepared by the caller escape + // and therefore allocate. This way, Hash(file, line) runs + // without any allocation. It should be clear from the + // source code calling Hash what the bad argument was. + panic("bisect.Hash: unexpected argument type") + case string: + h = fnvString(h, v) + case byte: + h = fnv(h, v) + case int: + h = fnvUint64(h, uint64(v)) + case uint: + h = fnvUint64(h, uint64(v)) + case int32: + h = fnvUint32(h, uint32(v)) + case uint32: + h = fnvUint32(h, v) + case int64: + h = fnvUint64(h, uint64(v)) + case uint64: + h = fnvUint64(h, v) + case uintptr: + h = fnvUint64(h, uint64(v)) + case []string: + for _, x := range v { + h = fnvString(h, x) + } + case []byte: + for _, x := range v { + h = fnv(h, x) + } + case []int: + for _, x := range v { + h = fnvUint64(h, uint64(x)) + } + case []uint: + for _, x := range v { + h = fnvUint64(h, uint64(x)) + } + case []int32: + for _, x := range v { + h = fnvUint32(h, uint32(x)) + } + case []uint32: + for _, x := range v { + h = fnvUint32(h, x) + } + case []int64: + for _, x := range v { + h = fnvUint64(h, uint64(x)) + } + case []uint64: + for _, x := range v { + h = fnvUint64(h, x) + } + case []uintptr: + for _, x := range v { + h = fnvUint64(h, uint64(x)) + } + } + } + return h +} + +// Trivial error implementation, here to avoid importing errors. + +// parseError is a trivial error implementation, +// defined here to avoid importing errors. +type parseError struct{ text string } + +func (e *parseError) Error() string { return e.text } + +// FNV-1a implementation. See Go's hash/fnv/fnv.go. +// Copied here for simplicity (can handle integers more directly) +// and to avoid importing hash/fnv. + +const ( + offset64 uint64 = 14695981039346656037 + prime64 uint64 = 1099511628211 +) + +func fnv(h uint64, x byte) uint64 { + h ^= uint64(x) + h *= prime64 + return h +} + +func fnvString(h uint64, x string) uint64 { + for i := 0; i < len(x); i++ { + h ^= uint64(x[i]) + h *= prime64 + } + return h +} + +func fnvUint64(h uint64, x uint64) uint64 { + for i := 0; i < 8; i++ { + h ^= x & 0xFF + x >>= 8 + h *= prime64 + } + return h +} + +func fnvUint32(h uint64, x uint32) uint64 { + for i := 0; i < 4; i++ { + h ^= uint64(x & 0xFF) + x >>= 8 + h *= prime64 + } + return h +} + +// A dedup is a deduplicator for call stacks, so that we only print +// a report for new call stacks, not for call stacks we've already +// reported. +// +// It has two modes: an approximate but lock-free mode that +// may still emit some duplicates, and a precise mode that uses +// a lock and never emits duplicates. +type dedup struct { + // 128-entry 4-way, lossy cache for seenLossy + recent [128][4]uint64 + + // complete history for seen + mu sync.Mutex + m map[uint64]bool +} + +// seen records that h has now been seen and reports whether it was seen before. +// When seen returns false, the caller is expected to print a report for h. +func (d *dedup) seen(h uint64) bool { + d.mu.Lock() + if d.m == nil { + d.m = make(map[uint64]bool) + } + seen := d.m[h] + d.m[h] = true + d.mu.Unlock() + return seen +} + +// seenLossy is a variant of seen that avoids a lock by using a cache of recently seen hashes. +// Each cache entry is N-way set-associative: h can appear in any of the slots. +// If h does not appear in any of them, then it is inserted into a random slot, +// overwriting whatever was there before. +func (d *dedup) seenLossy(h uint64) bool { + cache := &d.recent[uint(h)%uint(len(d.recent))] + for i := 0; i < len(cache); i++ { + if atomic.LoadUint64(&cache[i]) == h { + return true + } + } + + // Compute index in set to evict as hash of current set. + ch := offset64 + for _, x := range cache { + ch = fnvUint64(ch, x) + } + atomic.StoreUint64(&cache[uint(ch)%uint(len(cache))], h) + return false +} diff --git a/internal/go/src/buildcfg/cfg.go b/internal/go/src/buildcfg/cfg.go new file mode 100755 index 0000000..89fd74e --- /dev/null +++ b/internal/go/src/buildcfg/cfg.go @@ -0,0 +1,467 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package buildcfg provides access to the build configuration +// described by the current environment. It is for use by build tools +// such as cmd/go or cmd/compile and for setting up go/build's Default context. +// +// Note that it does NOT provide access to the build configuration used to +// build the currently-running binary. For that, use runtime.GOOS etc +// as well as internal/goexperiment. +package buildcfg + +import ( + "fmt" + "os" + "path/filepath" + "strconv" + "strings" +) + +var ( + GOROOT = os.Getenv("GOROOT") // cached for efficiency + GOARCH = envOr("GOARCH", defaultGOARCH) + GOOS = envOr("GOOS", defaultGOOS) + GO386 = envOr("GO386", DefaultGO386) + GOAMD64 = goamd64() + GOARM = goarm() + GOARM64 = goarm64() + GOMIPS = gomips() + GOMIPS64 = gomips64() + GOPPC64 = goppc64() + GORISCV64 = goriscv64() + GOWASM = gowasm() + ToolTags = toolTags() + GO_LDSO = defaultGO_LDSO + GOFIPS140 = gofips140() + Version = version +) + +// Error is one of the errors found (if any) in the build configuration. +var Error error + +// Check exits the program with a fatal error if Error is non-nil. +func Check() { + if Error != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", filepath.Base(os.Args[0]), Error) + os.Exit(2) + } +} + +func envOr(key, value string) string { + if x := os.Getenv(key); x != "" { + return x + } + return value +} + +func goamd64() int { + switch v := envOr("GOAMD64", DefaultGOAMD64); v { + case "v1": + return 1 + case "v2": + return 2 + case "v3": + return 3 + case "v4": + return 4 + } + Error = fmt.Errorf("invalid GOAMD64: must be v1, v2, v3, v4") + return int(DefaultGOAMD64[len("v")] - '0') +} + +func gofips140() string { + v := envOr("GOFIPS140", DefaultGOFIPS140) + switch v { + case "off", "latest", "inprocess", "certified": + return v + } + if isFIPSVersion(v) { + return v + } + Error = fmt.Errorf("invalid GOFIPS140: must be off, latest, inprocess, certified, or v1.Y.Z") + return DefaultGOFIPS140 +} + +// isFIPSVersion reports whether v is a valid FIPS version, +// of the form v1.Y.Z or v1.Y.Z-hhhhhhhh or v1.Y.Z-rcN. +func isFIPSVersion(v string) bool { + v, ok := strings.CutPrefix(v, "v1.") + if !ok { + return false + } + if v, ok = cutNum(v); !ok { + return false + } + if v, ok = strings.CutPrefix(v, "."); !ok { + return false + } + if v, ok = cutNum(v); !ok { + return false + } + if v == "" { + return true + } + if v, ok = strings.CutPrefix(v, "-rc"); ok { + v, ok = cutNum(v) + return ok && v == "" + } + if v, ok = strings.CutPrefix(v, "-"); ok { + return len(v) == 8 + } + return false +} + +// cutNum skips the leading text matching [0-9]+ +// in s, returning the rest and whether such text was found. +func cutNum(s string) (rest string, ok bool) { + i := 0 + for i < len(s) && '0' <= s[i] && s[i] <= '9' { + i++ + } + return s[i:], i > 0 +} + +type GoarmFeatures struct { + Version int + SoftFloat bool +} + +func (g GoarmFeatures) String() string { + armStr := strconv.Itoa(g.Version) + if g.SoftFloat { + armStr += ",softfloat" + } else { + armStr += ",hardfloat" + } + return armStr +} + +func goarm() (g GoarmFeatures) { + const ( + softFloatOpt = ",softfloat" + hardFloatOpt = ",hardfloat" + ) + def := DefaultGOARM + if GOOS == "android" && GOARCH == "arm" { + // Android arm devices always support GOARM=7. + def = "7" + } + v := envOr("GOARM", def) + + floatSpecified := false + if strings.HasSuffix(v, softFloatOpt) { + g.SoftFloat = true + floatSpecified = true + v = v[:len(v)-len(softFloatOpt)] + } + if strings.HasSuffix(v, hardFloatOpt) { + floatSpecified = true + v = v[:len(v)-len(hardFloatOpt)] + } + + switch v { + case "5": + g.Version = 5 + case "6": + g.Version = 6 + case "7": + g.Version = 7 + default: + Error = fmt.Errorf("invalid GOARM: must start with 5, 6, or 7, and may optionally end in either %q or %q", hardFloatOpt, softFloatOpt) + g.Version = int(def[0] - '0') + } + + // 5 defaults to softfloat. 6 and 7 default to hardfloat. + if !floatSpecified && g.Version == 5 { + g.SoftFloat = true + } + return +} + +type Goarm64Features struct { + Version string + // Large Systems Extension + LSE bool + // ARM v8.0 Cryptographic Extension. It includes the following features: + // * FEAT_AES, which includes the AESD and AESE instructions. + // * FEAT_PMULL, which includes the PMULL, PMULL2 instructions. + // * FEAT_SHA1, which includes the SHA1* instructions. + // * FEAT_SHA256, which includes the SHA256* instructions. + Crypto bool +} + +func (g Goarm64Features) String() string { + arm64Str := g.Version + if g.LSE { + arm64Str += ",lse" + } + if g.Crypto { + arm64Str += ",crypto" + } + return arm64Str +} + +func ParseGoarm64(v string) (g Goarm64Features, e error) { + const ( + lseOpt = ",lse" + cryptoOpt = ",crypto" + ) + + g.LSE = false + g.Crypto = false + // We allow any combination of suffixes, in any order + for { + if strings.HasSuffix(v, lseOpt) { + g.LSE = true + v = v[:len(v)-len(lseOpt)] + continue + } + + if strings.HasSuffix(v, cryptoOpt) { + g.Crypto = true + v = v[:len(v)-len(cryptoOpt)] + continue + } + + break + } + + switch v { + case "v8.0": + g.Version = v + case "v8.1", "v8.2", "v8.3", "v8.4", "v8.5", "v8.6", "v8.7", "v8.8", "v8.9", + "v9.0", "v9.1", "v9.2", "v9.3", "v9.4", "v9.5": + g.Version = v + // LSE extension is mandatory starting from 8.1 + g.LSE = true + default: + e = fmt.Errorf("invalid GOARM64: must start with v8.{0-9} or v9.{0-5} and may optionally end in %q and/or %q", + lseOpt, cryptoOpt) + g.Version = DefaultGOARM64 + } + + return +} + +func goarm64() (g Goarm64Features) { + g, Error = ParseGoarm64(envOr("GOARM64", DefaultGOARM64)) + return +} + +// Returns true if g supports giving ARM64 ISA +// Note that this function doesn't accept / test suffixes (like ",lse" or ",crypto") +func (g Goarm64Features) Supports(s string) bool { + // We only accept "v{8-9}.{0-9}. Everything else is malformed. + if len(s) != 4 { + return false + } + + major := s[1] + minor := s[3] + + // We only accept "v{8-9}.{0-9}. Everything else is malformed. + if major < '8' || major > '9' || + minor < '0' || minor > '9' || + s[0] != 'v' || s[2] != '.' { + return false + } + + g_major := g.Version[1] + g_minor := g.Version[3] + + if major == g_major { + return minor <= g_minor + } else if g_major == '9' { + // v9.0 diverged from v8.5. This means we should compare with g_minor increased by five. + return minor <= g_minor+5 + } else { + return false + } +} + +func gomips() string { + switch v := envOr("GOMIPS", DefaultGOMIPS); v { + case "hardfloat", "softfloat": + return v + } + Error = fmt.Errorf("invalid GOMIPS: must be hardfloat, softfloat") + return DefaultGOMIPS +} + +func gomips64() string { + switch v := envOr("GOMIPS64", DefaultGOMIPS64); v { + case "hardfloat", "softfloat": + return v + } + Error = fmt.Errorf("invalid GOMIPS64: must be hardfloat, softfloat") + return DefaultGOMIPS64 +} + +func goppc64() int { + switch v := envOr("GOPPC64", DefaultGOPPC64); v { + case "power8": + return 8 + case "power9": + return 9 + case "power10": + return 10 + } + Error = fmt.Errorf("invalid GOPPC64: must be power8, power9, power10") + return int(DefaultGOPPC64[len("power")] - '0') +} + +func goriscv64() int { + switch v := envOr("GORISCV64", DefaultGORISCV64); v { + case "rva20u64": + return 20 + case "rva22u64": + return 22 + case "rva23u64": + return 23 + } + Error = fmt.Errorf("invalid GORISCV64: must be rva20u64, rva22u64, rva23u64") + v := DefaultGORISCV64[len("rva"):] + i := strings.IndexFunc(v, func(r rune) bool { + return r < '0' || r > '9' + }) + year, _ := strconv.Atoi(v[:i]) + return year +} + +type gowasmFeatures struct { + // Legacy features, now always enabled + //SatConv bool + //SignExt bool +} + +func (f gowasmFeatures) String() string { + var flags []string + return strings.Join(flags, ",") +} + +func gowasm() (f gowasmFeatures) { + for opt := range strings.SplitSeq(envOr("GOWASM", ""), ",") { + switch opt { + case "satconv": + // ignore, always enabled + case "signext": + // ignore, always enabled + case "": + // ignore + default: + Error = fmt.Errorf("invalid GOWASM: no such feature %q", opt) + } + } + return +} + +func Getgoextlinkenabled() string { + return envOr("GO_EXTLINK_ENABLED", defaultGO_EXTLINK_ENABLED) +} + +func toolTags() []string { + tags := experimentTags() + tags = append(tags, gogoarchTags()...) + return tags +} + +func experimentTags() []string { + var list []string + // For each experiment that has been enabled in the toolchain, define a + // build tag with the same name but prefixed by "goexperiment." which can be + // used for compiling alternative files for the experiment. This allows + // changes for the experiment, like extra struct fields in the runtime, + // without affecting the base non-experiment code at all. + for _, exp := range Experiment.Enabled() { + list = append(list, "goexperiment."+exp) + } + return list +} + +// GOGOARCH returns the name and value of the GO$GOARCH setting. +// For example, if GOARCH is "amd64" it might return "GOAMD64", "v2". +func GOGOARCH() (name, value string) { + switch GOARCH { + case "386": + return "GO386", GO386 + case "amd64": + return "GOAMD64", fmt.Sprintf("v%d", GOAMD64) + case "arm": + return "GOARM", GOARM.String() + case "arm64": + return "GOARM64", GOARM64.String() + case "mips", "mipsle": + return "GOMIPS", GOMIPS + case "mips64", "mips64le": + return "GOMIPS64", GOMIPS64 + case "ppc64", "ppc64le": + return "GOPPC64", fmt.Sprintf("power%d", GOPPC64) + case "riscv64": + return "GORISCV64", fmt.Sprintf("rva%du64", GORISCV64) + case "wasm": + return "GOWASM", GOWASM.String() + } + return "", "" +} + +func gogoarchTags() []string { + switch GOARCH { + case "386": + return []string{GOARCH + "." + GO386} + case "amd64": + var list []string + for i := 1; i <= GOAMD64; i++ { + list = append(list, fmt.Sprintf("%s.v%d", GOARCH, i)) + } + return list + case "arm": + var list []string + for i := 5; i <= GOARM.Version; i++ { + list = append(list, fmt.Sprintf("%s.%d", GOARCH, i)) + } + return list + case "arm64": + var list []string + major := int(GOARM64.Version[1] - '0') + minor := int(GOARM64.Version[3] - '0') + for i := 0; i <= minor; i++ { + list = append(list, fmt.Sprintf("%s.v%d.%d", GOARCH, major, i)) + } + // ARM64 v9.x also includes support of v8.x+5 (i.e. v9.1 includes v8.(1+5) = v8.6). + if major == 9 { + for i := 0; i <= minor+5 && i <= 9; i++ { + list = append(list, fmt.Sprintf("%s.v%d.%d", GOARCH, 8, i)) + } + } + return list + case "mips", "mipsle": + return []string{GOARCH + "." + GOMIPS} + case "mips64", "mips64le": + return []string{GOARCH + "." + GOMIPS64} + case "ppc64", "ppc64le": + var list []string + for i := 8; i <= GOPPC64; i++ { + list = append(list, fmt.Sprintf("%s.power%d", GOARCH, i)) + } + return list + case "riscv64": + list := []string{GOARCH + "." + "rva20u64"} + if GORISCV64 >= 22 { + list = append(list, GOARCH+"."+"rva22u64") + } + if GORISCV64 >= 23 { + list = append(list, GOARCH+"."+"rva23u64") + } + return list + case "wasm": + var list []string + // SatConv is always enabled + list = append(list, GOARCH+".satconv") + // SignExt is always enabled + list = append(list, GOARCH+".signext") + return list + } + return nil +} diff --git a/internal/go/src/buildcfg/exp.go b/internal/go/src/buildcfg/exp.go new file mode 100755 index 0000000..5921962 --- /dev/null +++ b/internal/go/src/buildcfg/exp.go @@ -0,0 +1,203 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package buildcfg + +import ( + "fmt" + "reflect" + "strings" + + "loov.dev/lensm/internal/go/src/goexperiment" +) + +// ExperimentFlags represents a set of GOEXPERIMENT flags relative to a baseline +// (platform-default) experiment configuration. +type ExperimentFlags struct { + goexperiment.Flags + baseline goexperiment.Flags +} + +// Experiment contains the toolchain experiments enabled for the +// current build. +// +// (This is not necessarily the set of experiments the compiler itself +// was built with.) +// +// Experiment.baseline specifies the experiment flags that are enabled by +// default in the current toolchain. This is, in effect, the "control" +// configuration and any variation from this is an experiment. +var Experiment ExperimentFlags = func() ExperimentFlags { + flags, err := ParseGOEXPERIMENT(GOOS, GOARCH, envOr("GOEXPERIMENT", defaultGOEXPERIMENT)) + if err != nil { + Error = err + return ExperimentFlags{} + } + return *flags +}() + +// DefaultGOEXPERIMENT is the embedded default GOEXPERIMENT string. +// It is not guaranteed to be canonical. +const DefaultGOEXPERIMENT = defaultGOEXPERIMENT + +// FramePointerEnabled enables the use of platform conventions for +// saving frame pointers. +// +// This used to be an experiment, but now it's always enabled on +// platforms that support it. +// +// Note: must agree with runtime.framepointer_enabled. +var FramePointerEnabled = GOARCH == "amd64" || GOARCH == "arm64" + +// ParseGOEXPERIMENT parses a (GOOS, GOARCH, GOEXPERIMENT) +// configuration tuple and returns the enabled and baseline experiment +// flag sets. +// +// TODO(mdempsky): Move to [internal/goexperiment]. +func ParseGOEXPERIMENT(goos, goarch, goexp string) (*ExperimentFlags, error) { + // regabiSupported is set to true on platforms where register ABI is + // supported and enabled by default. + // regabiAlwaysOn is set to true on platforms where register ABI is + // always on. + var regabiSupported, regabiAlwaysOn bool + switch goarch { + case "amd64", "arm64", "loong64", "ppc64le", "ppc64", "riscv64": + regabiAlwaysOn = true + regabiSupported = true + case "s390x": + regabiSupported = true + } + + // Older versions (anything before V16) of dsymutil don't handle + // the .debug_rnglists section in DWARF5. See + // https://github.com/golang/go/issues/26379#issuecomment-2677068742 + // for more context. This disables all DWARF5 on mac, which is not + // ideal (would be better to disable just for cases where we know + // the build will use external linking). In the GOOS=aix case, the + // XCOFF format (as far as can be determined) doesn't seem to + // support the necessary section subtypes for DWARF-specific + // things like .debug_addr (needed for DWARF 5). + dwarf5Supported := (goos != "darwin" && goos != "ios" && goos != "aix") + + baseline := goexperiment.Flags{ + RegabiWrappers: regabiSupported, + RegabiArgs: regabiSupported, + Dwarf5: dwarf5Supported, + RandomizedHeapBase64: true, + GreenTeaGC: true, + } + flags := &ExperimentFlags{ + Flags: baseline, + baseline: baseline, + } + + // Pick up any changes to the baseline configuration from the + // GOEXPERIMENT environment. This can be set at make.bash time + // and overridden at build time. + if goexp != "" { + // Create a map of known experiment names. + names := make(map[string]func(bool)) + rv := reflect.ValueOf(&flags.Flags).Elem() + rt := rv.Type() + for i := 0; i < rt.NumField(); i++ { + field := rv.Field(i) + names[strings.ToLower(rt.Field(i).Name)] = field.SetBool + } + + // "regabi" is an alias for all working regabi + // subexperiments, and not an experiment itself. Doing + // this as an alias make both "regabi" and "noregabi" + // do the right thing. + names["regabi"] = func(v bool) { + flags.RegabiWrappers = v + flags.RegabiArgs = v + } + + // Parse names. + for f := range strings.SplitSeq(goexp, ",") { + if f == "" { + continue + } + if f == "none" { + // GOEXPERIMENT=none disables all experiment flags. + // This is used by cmd/dist, which doesn't know how + // to build with any experiment flags. + flags.Flags = goexperiment.Flags{} + continue + } + val := true + if strings.HasPrefix(f, "no") { + f, val = f[2:], false + } + set, ok := names[f] + if !ok { + return nil, fmt.Errorf("unknown GOEXPERIMENT %s", f) + } + set(val) + } + } + + if regabiAlwaysOn { + flags.RegabiWrappers = true + flags.RegabiArgs = true + } + // regabi is only supported on amd64, arm64, loong64, riscv64, s390x, ppc64 and ppc64le. + if !regabiSupported { + flags.RegabiWrappers = false + flags.RegabiArgs = false + } + // Check regabi dependencies. + if flags.RegabiArgs && !flags.RegabiWrappers { + return nil, fmt.Errorf("GOEXPERIMENT regabiargs requires regabiwrappers") + } + return flags, nil +} + +// String returns the canonical GOEXPERIMENT string to enable this experiment +// configuration. (Experiments in the same state as in the baseline are elided.) +func (exp *ExperimentFlags) String() string { + return strings.Join(expList(&exp.Flags, &exp.baseline, false), ",") +} + +// expList returns the list of lower-cased experiment names for +// experiments that differ from base. base may be nil to indicate no +// experiments. If all is true, then include all experiment flags, +// regardless of base. +func expList(exp, base *goexperiment.Flags, all bool) []string { + var list []string + rv := reflect.ValueOf(exp).Elem() + var rBase reflect.Value + if base != nil { + rBase = reflect.ValueOf(base).Elem() + } + rt := rv.Type() + for i := 0; i < rt.NumField(); i++ { + name := strings.ToLower(rt.Field(i).Name) + val := rv.Field(i).Bool() + baseVal := false + if base != nil { + baseVal = rBase.Field(i).Bool() + } + if all || val != baseVal { + if val { + list = append(list, name) + } else { + list = append(list, "no"+name) + } + } + } + return list +} + +// Enabled returns a list of enabled experiments, as +// lower-cased experiment names. +func (exp *ExperimentFlags) Enabled() []string { + return expList(&exp.Flags, nil, false) +} + +// All returns a list of all experiment settings. +// Disabled experiments appear in the list prefixed by "no". +func (exp *ExperimentFlags) All() []string { + return expList(&exp.Flags, nil, true) +} diff --git a/internal/go/src/buildcfg/zbootstrap.go b/internal/go/src/buildcfg/zbootstrap.go new file mode 100755 index 0000000..95cda6e --- /dev/null +++ b/internal/go/src/buildcfg/zbootstrap.go @@ -0,0 +1,22 @@ +// Code generated by go tool dist; DO NOT EDIT. + +package buildcfg + +import "runtime" + +const DefaultGO386 = `sse2` +const DefaultGOAMD64 = `v1` +const DefaultGOARM = `7` +const DefaultGOARM64 = `v8.0` +const DefaultGOMIPS = `hardfloat` +const DefaultGOMIPS64 = `hardfloat` +const DefaultGOPPC64 = `power8` +const DefaultGORISCV64 = `rva20u64` +const defaultGOEXPERIMENT = `` +const defaultGO_EXTLINK_ENABLED = `` +const defaultGO_LDSO = `` +const version = `go1.26.4` +const defaultGOOS = runtime.GOOS +const defaultGOARCH = runtime.GOARCH +const DefaultGOFIPS140 = `off` +const DefaultCGO_ENABLED = "" diff --git a/internal/go/src/disasm/disasm.go b/internal/go/src/disasm/disasm.go new file mode 100755 index 0000000..f6e6852 --- /dev/null +++ b/internal/go/src/disasm/disasm.go @@ -0,0 +1,470 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package disasm provides disassembly routines. +// +// It is broken out from cmd/internal/objfile so tools that don't need +// disassembling don't need to depend on x/arch disassembler code. +package disasm + +import ( + "bufio" + "bytes" + "container/list" + "encoding/binary" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "sort" + "strings" + "text/tabwriter" + + "loov.dev/lensm/internal/go/src/objfile" + "loov.dev/lensm/internal/go/src/src" + + "golang.org/x/arch/arm/armasm" + "golang.org/x/arch/arm64/arm64asm" + "golang.org/x/arch/loong64/loong64asm" + "golang.org/x/arch/ppc64/ppc64asm" + "golang.org/x/arch/riscv64/riscv64asm" + "golang.org/x/arch/s390x/s390xasm" + "golang.org/x/arch/x86/x86asm" +) + +// Disasm is a disassembler for a given File. +type Disasm struct { + syms []objfile.Sym // symbols in file, sorted by address + pcln objfile.Liner // pcln table + text []byte // bytes of text segment (actual instructions) + textStart uint64 // start PC of text + textEnd uint64 // end PC of text + goarch string // GOARCH string + disasm disasmFunc // disassembler function for goarch + byteOrder binary.ByteOrder // byte order for goarch +} + +// DisasmForFile returns a disassembler for the file f. +func DisasmForFile(f *objfile.File) (*Disasm, error) { + return disasmForEntry(f.Entries()[0]) +} + +func disasmForEntry(e *objfile.Entry) (*Disasm, error) { + syms, err := e.Symbols() + if err != nil { + return nil, err + } + + pcln, err := e.PCLineTable() + if err != nil { + return nil, err + } + + textStart, textBytes, err := e.Text() + if err != nil { + return nil, err + } + + goarch := e.GOARCH() + disasm := disasms[goarch] + byteOrder := byteOrders[goarch] + if disasm == nil || byteOrder == nil { + return nil, fmt.Errorf("unsupported architecture %q", goarch) + } + + // Filter out section symbols, overwriting syms in place. + keep := syms[:0] + for _, sym := range syms { + switch sym.Name { + case "runtime.text", "text", "_text", "runtime.etext", "etext", "_etext": + // drop + default: + keep = append(keep, sym) + } + } + syms = keep + d := &Disasm{ + syms: syms, + pcln: pcln, + text: textBytes, + textStart: textStart, + textEnd: textStart + uint64(len(textBytes)), + goarch: goarch, + disasm: disasm, + byteOrder: byteOrder, + } + + return d, nil +} + +// lookup finds the symbol name containing addr. +func (d *Disasm) lookup(addr uint64) (name string, base uint64) { + i := sort.Search(len(d.syms), func(i int) bool { return addr < d.syms[i].Addr }) + if i > 0 { + s := d.syms[i-1] + if s.Addr != 0 && s.Addr <= addr && addr < s.Addr+uint64(s.Size) { + return s.Name, s.Addr + } + } + return "", 0 +} + +// base returns the final element in the path. +// It works on both Windows and Unix paths, +// regardless of host operating system. +func base(path string) string { + path = path[strings.LastIndex(path, "/")+1:] + path = path[strings.LastIndex(path, `\`)+1:] + return path +} + +// CachedFile contains the content of a file split into lines. +type CachedFile struct { + FileName string + Lines [][]byte +} + +// FileCache is a simple LRU cache of file contents. +type FileCache struct { + files *list.List + maxLen int +} + +// NewFileCache returns a FileCache which can contain up to maxLen cached file contents. +func NewFileCache(maxLen int) *FileCache { + return &FileCache{ + files: list.New(), + maxLen: maxLen, + } +} + +// Line returns the source code line for the given file and line number. +// If the file is not already cached, reads it, inserts it into the cache, +// and removes the least recently used file if necessary. +// If the file is in cache, it is moved to the front of the list. +func (fc *FileCache) Line(filename string, line int) ([]byte, error) { + if filepath.Ext(filename) != ".go" { + return nil, nil + } + + // Clean filenames returned by src.Pos.SymFilename() + // or src.PosBase.SymFilename() removing + // the leading src.FileSymPrefix. + filename = strings.TrimPrefix(filename, src.FileSymPrefix) + + // Expand literal "$GOROOT" rewritten by obj.AbsFile() + filename = filepath.Clean(os.ExpandEnv(filename)) + + var cf *CachedFile + var e *list.Element + + for e = fc.files.Front(); e != nil; e = e.Next() { + cf = e.Value.(*CachedFile) + if cf.FileName == filename { + break + } + } + + if e == nil { + content, err := os.ReadFile(filename) + if err != nil { + return nil, err + } + + cf = &CachedFile{ + FileName: filename, + Lines: bytes.Split(content, []byte{'\n'}), + } + fc.files.PushFront(cf) + + if fc.files.Len() >= fc.maxLen { + fc.files.Remove(fc.files.Back()) + } + } else { + fc.files.MoveToFront(e) + } + + // because //line directives can be out-of-range. (#36683) + if line-1 >= len(cf.Lines) || line-1 < 0 { + return nil, nil + } + + return cf.Lines[line-1], nil +} + +// Print prints a disassembly of the file to w. +// If filter is non-nil, the disassembly only includes functions with names matching filter. +// If printCode is true, the disassembly includes corresponding source lines. +// The disassembly only includes functions that overlap the range [start, end). +func (d *Disasm) Print(w io.Writer, filter *regexp.Regexp, start, end uint64, printCode bool, gnuAsm bool) { + if start < d.textStart { + start = d.textStart + } + if end > d.textEnd { + end = d.textEnd + } + printed := false + bw := bufio.NewWriter(w) + + var fc *FileCache + if printCode { + fc = NewFileCache(8) + } + + tw := tabwriter.NewWriter(bw, 18, 8, 1, '\t', tabwriter.StripEscape) + for _, sym := range d.syms { + symStart := sym.Addr + symEnd := sym.Addr + uint64(sym.Size) + relocs := sym.Relocs + if sym.Code != 'T' && sym.Code != 't' || + symStart < d.textStart || + symEnd <= start || end <= symStart || + filter != nil && !filter.MatchString(sym.Name) { + continue + } + if printed { + fmt.Fprintf(bw, "\n") + } + printed = true + + file, _, _ := d.pcln.PCToLine(sym.Addr) + fmt.Fprintf(bw, "TEXT %s(SB) %s\n", sym.Name, file) + + if symEnd > end { + symEnd = end + } + code := d.text[:end-d.textStart] + + var lastFile string + var lastLine int + + d.Decode(symStart, symEnd, relocs, gnuAsm, func(pc, size uint64, file string, line int, text string) { + i := pc - d.textStart + + if printCode { + if file != lastFile || line != lastLine { + if srcLine, err := fc.Line(file, line); err == nil { + fmt.Fprintf(tw, "%s%s%s\n", []byte{tabwriter.Escape}, srcLine, []byte{tabwriter.Escape}) + } + + lastFile, lastLine = file, line + } + + fmt.Fprintf(tw, " %#x\t", pc) + } else { + fmt.Fprintf(tw, " %s:%d\t%#x\t", base(file), line, pc) + } + + if size%4 != 0 || d.goarch == "386" || d.goarch == "amd64" { + // Print instruction as bytes. + fmt.Fprintf(tw, "%x", code[i:i+size]) + } else { + // Print instruction as 32-bit words. + for j := uint64(0); j < size; j += 4 { + if j > 0 { + fmt.Fprintf(tw, " ") + } + fmt.Fprintf(tw, "%08x", d.byteOrder.Uint32(code[i+j:])) + } + } + fmt.Fprintf(tw, "\t%s\t\n", text) + }) + tw.Flush() + } + bw.Flush() +} + +// Decode disassembles the text segment range [start, end), calling f for each instruction. +func (d *Disasm) Decode(start, end uint64, relocs []objfile.Reloc, gnuAsm bool, f func(pc, size uint64, file string, line int, text string)) { + if start < d.textStart { + start = d.textStart + } + if end > d.textEnd { + end = d.textEnd + } + code := d.text[:end-d.textStart] + lookup := d.lookup + for pc := start; pc < end; { + i := pc - d.textStart + text, _, size := d.disasm(code[i:], pc, lookup, d.byteOrder, gnuAsm) + file, line, _ := d.pcln.PCToLine(pc) + sep := "\t" + for len(relocs) > 0 && relocs[0].Addr < i+uint64(size) { + text += sep + relocs[0].Stringer.String(pc-start) + sep = " " + relocs = relocs[1:] + } + f(pc, uint64(size), file, line, text) + pc += uint64(size) + } +} + +type lookupFunc = func(addr uint64) (sym string, base uint64) + +// A disasmFunc returns the formatted text, the canonical (decoder) mnemonic — +// e.g. "LD1" where the Go syntax spells it "VLD1" — and the instruction size. +type disasmFunc func(code []byte, pc uint64, lookup lookupFunc, ord binary.ByteOrder, _ bool) (text, mnemonic string, size int) + +func disasm_386(code []byte, pc uint64, lookup lookupFunc, _ binary.ByteOrder, gnuAsm bool) (string, string, int) { + return disasm_x86(code, pc, lookup, 32, gnuAsm) +} + +func disasm_amd64(code []byte, pc uint64, lookup lookupFunc, _ binary.ByteOrder, gnuAsm bool) (string, string, int) { + return disasm_x86(code, pc, lookup, 64, gnuAsm) +} + +func disasm_x86(code []byte, pc uint64, lookup lookupFunc, arch int, gnuAsm bool) (string, string, int) { + inst, err := x86asm.Decode(code, arch) + var text, mnemonic string + size := inst.Len + if err != nil || size == 0 || inst.Op == 0 { + size = 1 + text = "?" + } else { + mnemonic = inst.Op.String() + if gnuAsm { + text = fmt.Sprintf("%-36s // %s", x86asm.GoSyntax(inst, pc, lookup), x86asm.GNUSyntax(inst, pc, nil)) + } else { + text = x86asm.GoSyntax(inst, pc, lookup) + } + } + return text, mnemonic, size +} + +type textReader struct { + code []byte + pc uint64 +} + +func (r textReader) ReadAt(data []byte, off int64) (n int, err error) { + if off < 0 || uint64(off) < r.pc { + return 0, io.EOF + } + d := uint64(off) - r.pc + if d >= uint64(len(r.code)) { + return 0, io.EOF + } + n = copy(data, r.code[d:]) + if n < len(data) { + err = io.ErrUnexpectedEOF + } + return +} + +func disasm_arm(code []byte, pc uint64, lookup lookupFunc, _ binary.ByteOrder, gnuAsm bool) (string, string, int) { + inst, err := armasm.Decode(code, armasm.ModeARM) + var text, mnemonic string + size := inst.Len + if err != nil || size == 0 || inst.Op == 0 { + size = 4 + text = "?" + } else if mnemonic = inst.Op.String(); gnuAsm { + text = fmt.Sprintf("%-36s // %s", armasm.GoSyntax(inst, pc, lookup, textReader{code, pc}), armasm.GNUSyntax(inst)) + } else { + text = armasm.GoSyntax(inst, pc, lookup, textReader{code, pc}) + } + return text, mnemonic, size +} + +func disasm_arm64(code []byte, pc uint64, lookup lookupFunc, byteOrder binary.ByteOrder, gnuAsm bool) (string, string, int) { + inst, err := arm64asm.Decode(code) + var text, mnemonic string + if err != nil || inst.Op == 0 { + text = "?" + } else if mnemonic = inst.Op.String(); gnuAsm { + text = fmt.Sprintf("%-36s // %s", arm64asm.GoSyntax(inst, pc, lookup, textReader{code, pc}), arm64asm.GNUSyntax(inst)) + } else { + text = arm64asm.GoSyntax(inst, pc, lookup, textReader{code, pc}) + } + return text, mnemonic, 4 +} + +func disasm_loong64(code []byte, pc uint64, lookup lookupFunc, byteOrder binary.ByteOrder, gnuAsm bool) (string, string, int) { + inst, err := loong64asm.Decode(code) + var text, mnemonic string + if err != nil || inst.Op == 0 { + text = "?" + } else if mnemonic = inst.Op.String(); gnuAsm { + text = fmt.Sprintf("%-36s // %s", loong64asm.GoSyntax(inst, pc, lookup), loong64asm.GNUSyntax(inst)) + } else { + text = loong64asm.GoSyntax(inst, pc, lookup) + } + return text, mnemonic, 4 +} + +func disasm_ppc64(code []byte, pc uint64, lookup lookupFunc, byteOrder binary.ByteOrder, gnuAsm bool) (string, string, int) { + inst, err := ppc64asm.Decode(code, byteOrder) + var text, mnemonic string + size := inst.Len + if err != nil || size == 0 { + size = 4 + text = "?" + } else { + mnemonic = inst.Op.String() + if gnuAsm { + text = fmt.Sprintf("%-36s // %s", ppc64asm.GoSyntax(inst, pc, lookup), ppc64asm.GNUSyntax(inst, pc)) + } else { + text = ppc64asm.GoSyntax(inst, pc, lookup) + } + } + return text, mnemonic, size +} + +func disasm_riscv64(code []byte, pc uint64, lookup lookupFunc, byteOrder binary.ByteOrder, gnuAsm bool) (string, string, int) { + inst, err := riscv64asm.Decode(code) + var text, mnemonic string + size := inst.Len + if err != nil || inst.Op == 0 { + size = 2 + text = "?" + } else if mnemonic = inst.Op.String(); gnuAsm { + text = fmt.Sprintf("%-36s // %s", riscv64asm.GoSyntax(inst, pc, lookup, textReader{code, pc}), riscv64asm.GNUSyntax(inst)) + } else { + text = riscv64asm.GoSyntax(inst, pc, lookup, textReader{code, pc}) + } + return text, mnemonic, size +} + +func disasm_s390x(code []byte, pc uint64, lookup lookupFunc, _ binary.ByteOrder, gnuAsm bool) (string, string, int) { + inst, err := s390xasm.Decode(code) + var text, mnemonic string + size := inst.Len + if err != nil || size == 0 || inst.Op == 0 { + size = 2 + text = "?" + } else { + mnemonic = inst.Op.String() + if gnuAsm { + text = fmt.Sprintf("%-36s // %s", s390xasm.GoSyntax(inst, pc, lookup), s390xasm.GNUSyntax(inst, pc)) + } else { + text = s390xasm.GoSyntax(inst, pc, lookup) + } + } + return text, mnemonic, size +} + +var disasms = map[string]disasmFunc{ + "386": disasm_386, + "amd64": disasm_amd64, + "arm": disasm_arm, + "arm64": disasm_arm64, + "loong64": disasm_loong64, + "ppc64": disasm_ppc64, + "ppc64le": disasm_ppc64, + "riscv64": disasm_riscv64, + "s390x": disasm_s390x, +} + +var byteOrders = map[string]binary.ByteOrder{ + "386": binary.LittleEndian, + "amd64": binary.LittleEndian, + "arm": binary.LittleEndian, + "arm64": binary.LittleEndian, + "loong64": binary.LittleEndian, + "ppc64": binary.BigEndian, + "ppc64le": binary.LittleEndian, + "riscv64": binary.LittleEndian, + "s390x": binary.BigEndian, +} diff --git a/internal/go/src/disasm/expose.go b/internal/go/src/disasm/expose.go new file mode 100644 index 0000000..3fad616 --- /dev/null +++ b/internal/go/src/disasm/expose.go @@ -0,0 +1,51 @@ +package disasm + +import ( + "strings" + + "loov.dev/lensm/internal/go/src/objfile" +) + +func (d *Disasm) Syms() []objfile.Sym { return d.syms } +func (d *Disasm) TextStart() uint64 { return d.textStart } +func (d *Disasm) TextEnd() uint64 { return d.textEnd } +func (d *Disasm) PCLN() objfile.Liner { return d.pcln } +func (d *Disasm) GOARCH() string { return d.goarch } + +// DecodeSyntax disassembles the text segment range [start, end), calling f for +// each instruction with Go assembler syntax and native (GNU) syntax separately. +// +// This is a lensm addition, re-applied on every `go generate` because upstream +// Decode only yields a single combined syntax. It relies on Decode's gnuAsm=true +// format of "%-36s // %s" (goText // gnuText) to split the two apart. +// mnemonic is the canonical decoder mnemonic (e.g. "LD1" where the Go syntax +// spells it "VLD1"), used for reference lookups; empty for undecodable bytes. +func (d *Disasm) DecodeSyntax(start, end uint64, relocs []objfile.Reloc, f func(pc, size uint64, file string, line int, goText, nativeText, mnemonic string)) { + if start < d.textStart { + start = d.textStart + } + if end > d.textEnd { + end = d.textEnd + } + code := d.text[:end-d.textStart] + lookup := d.lookup + for pc := start; pc < end; { + i := pc - d.textStart + combined, mnemonic, size := d.disasm(code[i:], pc, lookup, d.byteOrder, true) + goText, nativeText := combined, combined + if j := strings.Index(combined, " // "); j >= 0 { + goText = strings.TrimRight(combined[:j], " ") + nativeText = combined[j+len(" // "):] + } + file, line, _ := d.pcln.PCToLine(pc) + reloc := "" + sep := "\t" + for len(relocs) > 0 && relocs[0].Addr < i+uint64(size) { + reloc += sep + relocs[0].Stringer.String(pc-start) + sep = " " + relocs = relocs[1:] + } + f(pc, uint64(size), file, line, goText+reloc, nativeText+reloc, mnemonic) + pc += uint64(size) + } +} diff --git a/internal/go/src/goarch/gengoarch.go b/internal/go/src/goarch/gengoarch.go new file mode 100755 index 0000000..a52936e --- /dev/null +++ b/internal/go/src/goarch/gengoarch.go @@ -0,0 +1,60 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +package main + +import ( + "bytes" + "fmt" + "log" + "os" + "strings" +) + +var goarches []string + +func main() { + data, err := os.ReadFile("../../internal/syslist/syslist.go") + if err != nil { + log.Fatal(err) + } + const goarchPrefix = `var KnownArch = map[string]bool{` + inGOARCH := false + for _, line := range strings.Split(string(data), "\n") { + if strings.HasPrefix(line, goarchPrefix) { + inGOARCH = true + } else if inGOARCH && strings.HasPrefix(line, "}") { + break + } else if inGOARCH { + goarch := strings.Fields(line)[0] + goarch = strings.TrimPrefix(goarch, `"`) + goarch = strings.TrimSuffix(goarch, `":`) + goarches = append(goarches, goarch) + } + } + + for _, target := range goarches { + if target == "amd64p32" { + continue + } + var buf bytes.Buffer + fmt.Fprintf(&buf, "// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT.\n\n") + fmt.Fprintf(&buf, "//go:build %s\n\n", target) // must explicitly include target for bootstrapping purposes + fmt.Fprintf(&buf, "package goarch\n\n") + fmt.Fprintf(&buf, "const GOARCH = `%s`\n\n", target) + for _, goarch := range goarches { + value := 0 + if goarch == target { + value = 1 + } + fmt.Fprintf(&buf, "const Is%s = %d\n", strings.Title(goarch), value) + } + err := os.WriteFile("zgoarch_"+target+".go", buf.Bytes(), 0666) + if err != nil { + log.Fatal(err) + } + } +} diff --git a/internal/go/src/goarch/goarch.go b/internal/go/src/goarch/goarch.go new file mode 100755 index 0000000..efcf298 --- /dev/null +++ b/internal/go/src/goarch/goarch.go @@ -0,0 +1,65 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// package goarch contains GOARCH-specific constants. +package goarch + +// The next line makes 'go generate' write the zgoarch*.go files with +// per-arch information, including constants named $GOARCH for every +// GOARCH. The constant is 1 on the current system, 0 otherwise; multiplying +// by them is useful for defining GOARCH-specific constants. +// +//go:generate go run gengoarch.go + +// ArchFamilyType represents a family of one or more related architectures. +// For example, ppc64 and ppc64le are both members of the PPC64 family. +type ArchFamilyType int + +const ( + AMD64 ArchFamilyType = iota + ARM + ARM64 + I386 + LOONG64 + MIPS + MIPS64 + PPC64 + RISCV64 + S390X + WASM +) + +// PtrSize is the size of a pointer in bytes - unsafe.Sizeof(uintptr(0)) but as an ideal constant. +// It is also the size of the machine's native word size (that is, 4 on 32-bit systems, 8 on 64-bit). +const PtrSize = 4 << (^uintptr(0) >> 63) + +// PtrBits is bit width of a pointer. +const PtrBits = PtrSize * 8 + +// ArchFamily is the architecture family (AMD64, ARM, ...) +const ArchFamily ArchFamilyType = _ArchFamily + +// BigEndian reports whether the architecture is big-endian. +const BigEndian = IsArmbe|IsArm64be|IsMips|IsMips64|IsPpc|IsPpc64|IsS390|IsS390x|IsSparc|IsSparc64 == 1 + +// DefaultPhysPageSize is the default physical page size. +const DefaultPhysPageSize = _DefaultPhysPageSize + +// PCQuantum is the minimal unit for a program counter (1 on x86, 4 on most other systems). +// The various PC tables record PC deltas pre-divided by PCQuantum. +const PCQuantum = _PCQuantum + +// Int64Align is the required alignment for a 64-bit integer (4 on 32-bit systems, 8 on 64-bit). +const Int64Align = PtrSize + +// MinFrameSize is the size of the system-reserved words at the bottom +// of a frame (just above the architectural stack pointer). +// It is zero on x86 and PtrSize on most non-x86 (LR-based) systems. +// On PowerPC it is larger, to cover three more reserved words: +// the compiler word, the link editor word, and the TOC save word. +const MinFrameSize = _MinFrameSize + +// StackAlign is the required alignment of the SP register. +// The stack must be at least word aligned, but some architectures require more. +const StackAlign = _StackAlign diff --git a/internal/go/src/goarch/goarch_386.go b/internal/go/src/goarch/goarch_386.go new file mode 100755 index 0000000..c621421 --- /dev/null +++ b/internal/go/src/goarch/goarch_386.go @@ -0,0 +1,13 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = I386 + _DefaultPhysPageSize = 4096 + _PCQuantum = 1 + _MinFrameSize = 0 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_amd64.go b/internal/go/src/goarch/goarch_amd64.go new file mode 100755 index 0000000..911e3e7 --- /dev/null +++ b/internal/go/src/goarch/goarch_amd64.go @@ -0,0 +1,13 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = AMD64 + _DefaultPhysPageSize = 4096 + _PCQuantum = 1 + _MinFrameSize = 0 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_arm.go b/internal/go/src/goarch/goarch_arm.go new file mode 100755 index 0000000..a659171 --- /dev/null +++ b/internal/go/src/goarch/goarch_arm.go @@ -0,0 +1,13 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = ARM + _DefaultPhysPageSize = 65536 + _PCQuantum = 4 + _MinFrameSize = 4 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_arm64.go b/internal/go/src/goarch/goarch_arm64.go new file mode 100755 index 0000000..85d0b47 --- /dev/null +++ b/internal/go/src/goarch/goarch_arm64.go @@ -0,0 +1,13 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = ARM64 + _DefaultPhysPageSize = 65536 + _PCQuantum = 4 + _MinFrameSize = 8 + _StackAlign = 16 +) diff --git a/internal/go/src/goarch/goarch_loong64.go b/internal/go/src/goarch/goarch_loong64.go new file mode 100755 index 0000000..dae1f4d --- /dev/null +++ b/internal/go/src/goarch/goarch_loong64.go @@ -0,0 +1,15 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build loong64 + +package goarch + +const ( + _ArchFamily = LOONG64 + _DefaultPhysPageSize = 16384 + _PCQuantum = 4 + _MinFrameSize = 8 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_mips.go b/internal/go/src/goarch/goarch_mips.go new file mode 100755 index 0000000..59f3995 --- /dev/null +++ b/internal/go/src/goarch/goarch_mips.go @@ -0,0 +1,13 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = MIPS + _DefaultPhysPageSize = 65536 + _PCQuantum = 4 + _MinFrameSize = 4 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_mips64.go b/internal/go/src/goarch/goarch_mips64.go new file mode 100755 index 0000000..9e4f827 --- /dev/null +++ b/internal/go/src/goarch/goarch_mips64.go @@ -0,0 +1,13 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = MIPS64 + _DefaultPhysPageSize = 16384 + _PCQuantum = 4 + _MinFrameSize = 8 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_mips64le.go b/internal/go/src/goarch/goarch_mips64le.go new file mode 100755 index 0000000..9e4f827 --- /dev/null +++ b/internal/go/src/goarch/goarch_mips64le.go @@ -0,0 +1,13 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = MIPS64 + _DefaultPhysPageSize = 16384 + _PCQuantum = 4 + _MinFrameSize = 8 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_mipsle.go b/internal/go/src/goarch/goarch_mipsle.go new file mode 100755 index 0000000..3e6642b --- /dev/null +++ b/internal/go/src/goarch/goarch_mipsle.go @@ -0,0 +1,13 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = MIPS + _DefaultPhysPageSize = 65536 + _PCQuantum = 4 + _MinFrameSize = 4 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_ppc64.go b/internal/go/src/goarch/goarch_ppc64.go new file mode 100755 index 0000000..60cc846 --- /dev/null +++ b/internal/go/src/goarch/goarch_ppc64.go @@ -0,0 +1,13 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = PPC64 + _DefaultPhysPageSize = 65536 + _PCQuantum = 4 + _MinFrameSize = 32 + _StackAlign = 16 +) diff --git a/internal/go/src/goarch/goarch_ppc64le.go b/internal/go/src/goarch/goarch_ppc64le.go new file mode 100755 index 0000000..60cc846 --- /dev/null +++ b/internal/go/src/goarch/goarch_ppc64le.go @@ -0,0 +1,13 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = PPC64 + _DefaultPhysPageSize = 65536 + _PCQuantum = 4 + _MinFrameSize = 32 + _StackAlign = 16 +) diff --git a/internal/go/src/goarch/goarch_riscv64.go b/internal/go/src/goarch/goarch_riscv64.go new file mode 100755 index 0000000..468f9a6 --- /dev/null +++ b/internal/go/src/goarch/goarch_riscv64.go @@ -0,0 +1,13 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = RISCV64 + _DefaultPhysPageSize = 4096 + _PCQuantum = 2 + _MinFrameSize = 8 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_s390x.go b/internal/go/src/goarch/goarch_s390x.go new file mode 100755 index 0000000..20c5705 --- /dev/null +++ b/internal/go/src/goarch/goarch_s390x.go @@ -0,0 +1,13 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = S390X + _DefaultPhysPageSize = 4096 + _PCQuantum = 2 + _MinFrameSize = 8 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/goarch_wasm.go b/internal/go/src/goarch/goarch_wasm.go new file mode 100755 index 0000000..98618d6 --- /dev/null +++ b/internal/go/src/goarch/goarch_wasm.go @@ -0,0 +1,13 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goarch + +const ( + _ArchFamily = WASM + _DefaultPhysPageSize = 65536 + _PCQuantum = 1 + _MinFrameSize = 0 + _StackAlign = PtrSize +) diff --git a/internal/go/src/goarch/zgoarch_386.go b/internal/go/src/goarch/zgoarch_386.go new file mode 100755 index 0000000..4a9b0e6 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_386.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build 386 + +package goarch + +const GOARCH = `386` + +const Is386 = 1 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_amd64.go b/internal/go/src/goarch/zgoarch_amd64.go new file mode 100755 index 0000000..7926392 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_amd64.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build amd64 + +package goarch + +const GOARCH = `amd64` + +const Is386 = 0 +const IsAmd64 = 1 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_arm.go b/internal/go/src/goarch/zgoarch_arm.go new file mode 100755 index 0000000..6c03b8b --- /dev/null +++ b/internal/go/src/goarch/zgoarch_arm.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build arm + +package goarch + +const GOARCH = `arm` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 1 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_arm64.go b/internal/go/src/goarch/zgoarch_arm64.go new file mode 100755 index 0000000..ad342d7 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_arm64.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build arm64 + +package goarch + +const GOARCH = `arm64` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 1 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_arm64be.go b/internal/go/src/goarch/zgoarch_arm64be.go new file mode 100755 index 0000000..0f26003 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_arm64be.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build arm64be + +package goarch + +const GOARCH = `arm64be` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 1 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_armbe.go b/internal/go/src/goarch/zgoarch_armbe.go new file mode 100755 index 0000000..6092fee --- /dev/null +++ b/internal/go/src/goarch/zgoarch_armbe.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build armbe + +package goarch + +const GOARCH = `armbe` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 1 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_loong64.go b/internal/go/src/goarch/zgoarch_loong64.go new file mode 100755 index 0000000..21c67e1 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_loong64.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build loong64 + +package goarch + +const GOARCH = `loong64` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 1 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_mips.go b/internal/go/src/goarch/zgoarch_mips.go new file mode 100755 index 0000000..0db1974 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_mips.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build mips + +package goarch + +const GOARCH = `mips` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 1 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_mips64.go b/internal/go/src/goarch/zgoarch_mips64.go new file mode 100755 index 0000000..738806f --- /dev/null +++ b/internal/go/src/goarch/zgoarch_mips64.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build mips64 + +package goarch + +const GOARCH = `mips64` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 1 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_mips64le.go b/internal/go/src/goarch/zgoarch_mips64le.go new file mode 100755 index 0000000..8de5beb --- /dev/null +++ b/internal/go/src/goarch/zgoarch_mips64le.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build mips64le + +package goarch + +const GOARCH = `mips64le` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 1 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_mips64p32.go b/internal/go/src/goarch/zgoarch_mips64p32.go new file mode 100755 index 0000000..ea461be --- /dev/null +++ b/internal/go/src/goarch/zgoarch_mips64p32.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build mips64p32 + +package goarch + +const GOARCH = `mips64p32` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 1 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_mips64p32le.go b/internal/go/src/goarch/zgoarch_mips64p32le.go new file mode 100755 index 0000000..15473ce --- /dev/null +++ b/internal/go/src/goarch/zgoarch_mips64p32le.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build mips64p32le + +package goarch + +const GOARCH = `mips64p32le` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 1 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_mipsle.go b/internal/go/src/goarch/zgoarch_mipsle.go new file mode 100755 index 0000000..4955142 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_mipsle.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build mipsle + +package goarch + +const GOARCH = `mipsle` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 1 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_ppc.go b/internal/go/src/goarch/zgoarch_ppc.go new file mode 100755 index 0000000..ec01763 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_ppc.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build ppc + +package goarch + +const GOARCH = `ppc` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 1 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_ppc64.go b/internal/go/src/goarch/zgoarch_ppc64.go new file mode 100755 index 0000000..39be392 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_ppc64.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build ppc64 + +package goarch + +const GOARCH = `ppc64` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 1 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_ppc64le.go b/internal/go/src/goarch/zgoarch_ppc64le.go new file mode 100755 index 0000000..5f959e0 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_ppc64le.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build ppc64le + +package goarch + +const GOARCH = `ppc64le` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 1 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_riscv.go b/internal/go/src/goarch/zgoarch_riscv.go new file mode 100755 index 0000000..8d81a14 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_riscv.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build riscv + +package goarch + +const GOARCH = `riscv` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 1 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_riscv64.go b/internal/go/src/goarch/zgoarch_riscv64.go new file mode 100755 index 0000000..1df989c --- /dev/null +++ b/internal/go/src/goarch/zgoarch_riscv64.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build riscv64 + +package goarch + +const GOARCH = `riscv64` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 1 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_s390.go b/internal/go/src/goarch/zgoarch_s390.go new file mode 100755 index 0000000..56815b9 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_s390.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build s390 + +package goarch + +const GOARCH = `s390` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 1 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_s390x.go b/internal/go/src/goarch/zgoarch_s390x.go new file mode 100755 index 0000000..e61e9bd --- /dev/null +++ b/internal/go/src/goarch/zgoarch_s390x.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build s390x + +package goarch + +const GOARCH = `s390x` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 1 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_sparc.go b/internal/go/src/goarch/zgoarch_sparc.go new file mode 100755 index 0000000..ee5b746 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_sparc.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build sparc + +package goarch + +const GOARCH = `sparc` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 1 +const IsSparc64 = 0 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_sparc64.go b/internal/go/src/goarch/zgoarch_sparc64.go new file mode 100755 index 0000000..519aaa1 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_sparc64.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build sparc64 + +package goarch + +const GOARCH = `sparc64` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 1 +const IsWasm = 0 diff --git a/internal/go/src/goarch/zgoarch_wasm.go b/internal/go/src/goarch/zgoarch_wasm.go new file mode 100755 index 0000000..25567a1 --- /dev/null +++ b/internal/go/src/goarch/zgoarch_wasm.go @@ -0,0 +1,32 @@ +// Code generated by gengoarch.go using 'go generate'. DO NOT EDIT. + +//go:build wasm + +package goarch + +const GOARCH = `wasm` + +const Is386 = 0 +const IsAmd64 = 0 +const IsAmd64p32 = 0 +const IsArm = 0 +const IsArmbe = 0 +const IsArm64 = 0 +const IsArm64be = 0 +const IsLoong64 = 0 +const IsMips = 0 +const IsMipsle = 0 +const IsMips64 = 0 +const IsMips64le = 0 +const IsMips64p32 = 0 +const IsMips64p32le = 0 +const IsPpc = 0 +const IsPpc64 = 0 +const IsPpc64le = 0 +const IsRiscv = 0 +const IsRiscv64 = 0 +const IsS390 = 0 +const IsS390x = 0 +const IsSparc = 0 +const IsSparc64 = 0 +const IsWasm = 1 diff --git a/internal/go/src/goexperiment/exp_arenas_off.go b/internal/go/src/goexperiment/exp_arenas_off.go new file mode 100755 index 0000000..01f5332 --- /dev/null +++ b/internal/go/src/goexperiment/exp_arenas_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.arenas + +package goexperiment + +const Arenas = false +const ArenasInt = 0 diff --git a/internal/go/src/goexperiment/exp_arenas_on.go b/internal/go/src/goexperiment/exp_arenas_on.go new file mode 100755 index 0000000..609dfbc --- /dev/null +++ b/internal/go/src/goexperiment/exp_arenas_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.arenas + +package goexperiment + +const Arenas = true +const ArenasInt = 1 diff --git a/internal/go/src/goexperiment/exp_boringcrypto_off.go b/internal/go/src/goexperiment/exp_boringcrypto_off.go new file mode 100755 index 0000000..de71267 --- /dev/null +++ b/internal/go/src/goexperiment/exp_boringcrypto_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.boringcrypto + +package goexperiment + +const BoringCrypto = false +const BoringCryptoInt = 0 diff --git a/internal/go/src/goexperiment/exp_boringcrypto_on.go b/internal/go/src/goexperiment/exp_boringcrypto_on.go new file mode 100755 index 0000000..ce476fa --- /dev/null +++ b/internal/go/src/goexperiment/exp_boringcrypto_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.boringcrypto + +package goexperiment + +const BoringCrypto = true +const BoringCryptoInt = 1 diff --git a/internal/go/src/goexperiment/exp_cgocheck2_off.go b/internal/go/src/goexperiment/exp_cgocheck2_off.go new file mode 100755 index 0000000..e99ad07 --- /dev/null +++ b/internal/go/src/goexperiment/exp_cgocheck2_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.cgocheck2 + +package goexperiment + +const CgoCheck2 = false +const CgoCheck2Int = 0 diff --git a/internal/go/src/goexperiment/exp_cgocheck2_on.go b/internal/go/src/goexperiment/exp_cgocheck2_on.go new file mode 100755 index 0000000..f6d1790 --- /dev/null +++ b/internal/go/src/goexperiment/exp_cgocheck2_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.cgocheck2 + +package goexperiment + +const CgoCheck2 = true +const CgoCheck2Int = 1 diff --git a/internal/go/src/goexperiment/exp_dwarf5_off.go b/internal/go/src/goexperiment/exp_dwarf5_off.go new file mode 100755 index 0000000..b89dd9f --- /dev/null +++ b/internal/go/src/goexperiment/exp_dwarf5_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.dwarf5 + +package goexperiment + +const Dwarf5 = false +const Dwarf5Int = 0 diff --git a/internal/go/src/goexperiment/exp_dwarf5_on.go b/internal/go/src/goexperiment/exp_dwarf5_on.go new file mode 100755 index 0000000..02b28d7 --- /dev/null +++ b/internal/go/src/goexperiment/exp_dwarf5_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.dwarf5 + +package goexperiment + +const Dwarf5 = true +const Dwarf5Int = 1 diff --git a/internal/go/src/goexperiment/exp_fieldtrack_off.go b/internal/go/src/goexperiment/exp_fieldtrack_off.go new file mode 100755 index 0000000..ccced94 --- /dev/null +++ b/internal/go/src/goexperiment/exp_fieldtrack_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.fieldtrack + +package goexperiment + +const FieldTrack = false +const FieldTrackInt = 0 diff --git a/internal/go/src/goexperiment/exp_fieldtrack_on.go b/internal/go/src/goexperiment/exp_fieldtrack_on.go new file mode 100755 index 0000000..a497567 --- /dev/null +++ b/internal/go/src/goexperiment/exp_fieldtrack_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.fieldtrack + +package goexperiment + +const FieldTrack = true +const FieldTrackInt = 1 diff --git a/internal/go/src/goexperiment/exp_goroutineleakprofile_off.go b/internal/go/src/goexperiment/exp_goroutineleakprofile_off.go new file mode 100755 index 0000000..63eafe9 --- /dev/null +++ b/internal/go/src/goexperiment/exp_goroutineleakprofile_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.goroutineleakprofile + +package goexperiment + +const GoroutineLeakProfile = false +const GoroutineLeakProfileInt = 0 diff --git a/internal/go/src/goexperiment/exp_goroutineleakprofile_on.go b/internal/go/src/goexperiment/exp_goroutineleakprofile_on.go new file mode 100755 index 0000000..28a662e --- /dev/null +++ b/internal/go/src/goexperiment/exp_goroutineleakprofile_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.goroutineleakprofile + +package goexperiment + +const GoroutineLeakProfile = true +const GoroutineLeakProfileInt = 1 diff --git a/internal/go/src/goexperiment/exp_greenteagc_off.go b/internal/go/src/goexperiment/exp_greenteagc_off.go new file mode 100755 index 0000000..dce9d8c --- /dev/null +++ b/internal/go/src/goexperiment/exp_greenteagc_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.greenteagc + +package goexperiment + +const GreenTeaGC = false +const GreenTeaGCInt = 0 diff --git a/internal/go/src/goexperiment/exp_greenteagc_on.go b/internal/go/src/goexperiment/exp_greenteagc_on.go new file mode 100755 index 0000000..10a007d --- /dev/null +++ b/internal/go/src/goexperiment/exp_greenteagc_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.greenteagc + +package goexperiment + +const GreenTeaGC = true +const GreenTeaGCInt = 1 diff --git a/internal/go/src/goexperiment/exp_heapminimum512kib_off.go b/internal/go/src/goexperiment/exp_heapminimum512kib_off.go new file mode 100755 index 0000000..d67c5bb --- /dev/null +++ b/internal/go/src/goexperiment/exp_heapminimum512kib_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.heapminimum512kib + +package goexperiment + +const HeapMinimum512KiB = false +const HeapMinimum512KiBInt = 0 diff --git a/internal/go/src/goexperiment/exp_heapminimum512kib_on.go b/internal/go/src/goexperiment/exp_heapminimum512kib_on.go new file mode 100755 index 0000000..2d29c98 --- /dev/null +++ b/internal/go/src/goexperiment/exp_heapminimum512kib_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.heapminimum512kib + +package goexperiment + +const HeapMinimum512KiB = true +const HeapMinimum512KiBInt = 1 diff --git a/internal/go/src/goexperiment/exp_jsonv2_off.go b/internal/go/src/goexperiment/exp_jsonv2_off.go new file mode 100755 index 0000000..a973a35 --- /dev/null +++ b/internal/go/src/goexperiment/exp_jsonv2_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.jsonv2 + +package goexperiment + +const JSONv2 = false +const JSONv2Int = 0 diff --git a/internal/go/src/goexperiment/exp_jsonv2_on.go b/internal/go/src/goexperiment/exp_jsonv2_on.go new file mode 100755 index 0000000..119d086 --- /dev/null +++ b/internal/go/src/goexperiment/exp_jsonv2_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.jsonv2 + +package goexperiment + +const JSONv2 = true +const JSONv2Int = 1 diff --git a/internal/go/src/goexperiment/exp_loopvar_off.go b/internal/go/src/goexperiment/exp_loopvar_off.go new file mode 100755 index 0000000..cfede54 --- /dev/null +++ b/internal/go/src/goexperiment/exp_loopvar_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.loopvar + +package goexperiment + +const LoopVar = false +const LoopVarInt = 0 diff --git a/internal/go/src/goexperiment/exp_loopvar_on.go b/internal/go/src/goexperiment/exp_loopvar_on.go new file mode 100755 index 0000000..e158e0a --- /dev/null +++ b/internal/go/src/goexperiment/exp_loopvar_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.loopvar + +package goexperiment + +const LoopVar = true +const LoopVarInt = 1 diff --git a/internal/go/src/goexperiment/exp_newinliner_off.go b/internal/go/src/goexperiment/exp_newinliner_off.go new file mode 100755 index 0000000..d94e736 --- /dev/null +++ b/internal/go/src/goexperiment/exp_newinliner_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.newinliner + +package goexperiment + +const NewInliner = false +const NewInlinerInt = 0 diff --git a/internal/go/src/goexperiment/exp_newinliner_on.go b/internal/go/src/goexperiment/exp_newinliner_on.go new file mode 100755 index 0000000..6777dbc --- /dev/null +++ b/internal/go/src/goexperiment/exp_newinliner_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.newinliner + +package goexperiment + +const NewInliner = true +const NewInlinerInt = 1 diff --git a/internal/go/src/goexperiment/exp_preemptibleloops_off.go b/internal/go/src/goexperiment/exp_preemptibleloops_off.go new file mode 100755 index 0000000..cddcc1b --- /dev/null +++ b/internal/go/src/goexperiment/exp_preemptibleloops_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.preemptibleloops + +package goexperiment + +const PreemptibleLoops = false +const PreemptibleLoopsInt = 0 diff --git a/internal/go/src/goexperiment/exp_preemptibleloops_on.go b/internal/go/src/goexperiment/exp_preemptibleloops_on.go new file mode 100755 index 0000000..7f474c0 --- /dev/null +++ b/internal/go/src/goexperiment/exp_preemptibleloops_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.preemptibleloops + +package goexperiment + +const PreemptibleLoops = true +const PreemptibleLoopsInt = 1 diff --git a/internal/go/src/goexperiment/exp_randomizedheapbase64_off.go b/internal/go/src/goexperiment/exp_randomizedheapbase64_off.go new file mode 100755 index 0000000..0a57853 --- /dev/null +++ b/internal/go/src/goexperiment/exp_randomizedheapbase64_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.randomizedheapbase64 + +package goexperiment + +const RandomizedHeapBase64 = false +const RandomizedHeapBase64Int = 0 diff --git a/internal/go/src/goexperiment/exp_randomizedheapbase64_on.go b/internal/go/src/goexperiment/exp_randomizedheapbase64_on.go new file mode 100755 index 0000000..10d59c7 --- /dev/null +++ b/internal/go/src/goexperiment/exp_randomizedheapbase64_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.randomizedheapbase64 + +package goexperiment + +const RandomizedHeapBase64 = true +const RandomizedHeapBase64Int = 1 diff --git a/internal/go/src/goexperiment/exp_regabiargs_off.go b/internal/go/src/goexperiment/exp_regabiargs_off.go new file mode 100755 index 0000000..a8c8536 --- /dev/null +++ b/internal/go/src/goexperiment/exp_regabiargs_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.regabiargs + +package goexperiment + +const RegabiArgs = false +const RegabiArgsInt = 0 diff --git a/internal/go/src/goexperiment/exp_regabiargs_on.go b/internal/go/src/goexperiment/exp_regabiargs_on.go new file mode 100755 index 0000000..def3b94 --- /dev/null +++ b/internal/go/src/goexperiment/exp_regabiargs_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.regabiargs + +package goexperiment + +const RegabiArgs = true +const RegabiArgsInt = 1 diff --git a/internal/go/src/goexperiment/exp_regabiwrappers_off.go b/internal/go/src/goexperiment/exp_regabiwrappers_off.go new file mode 100755 index 0000000..a65ed36 --- /dev/null +++ b/internal/go/src/goexperiment/exp_regabiwrappers_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.regabiwrappers + +package goexperiment + +const RegabiWrappers = false +const RegabiWrappersInt = 0 diff --git a/internal/go/src/goexperiment/exp_regabiwrappers_on.go b/internal/go/src/goexperiment/exp_regabiwrappers_on.go new file mode 100755 index 0000000..d525c9a --- /dev/null +++ b/internal/go/src/goexperiment/exp_regabiwrappers_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.regabiwrappers + +package goexperiment + +const RegabiWrappers = true +const RegabiWrappersInt = 1 diff --git a/internal/go/src/goexperiment/exp_runtimefreegc_off.go b/internal/go/src/goexperiment/exp_runtimefreegc_off.go new file mode 100755 index 0000000..195f031 --- /dev/null +++ b/internal/go/src/goexperiment/exp_runtimefreegc_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.runtimefreegc + +package goexperiment + +const RuntimeFreegc = false +const RuntimeFreegcInt = 0 diff --git a/internal/go/src/goexperiment/exp_runtimefreegc_on.go b/internal/go/src/goexperiment/exp_runtimefreegc_on.go new file mode 100755 index 0000000..2afe055 --- /dev/null +++ b/internal/go/src/goexperiment/exp_runtimefreegc_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.runtimefreegc + +package goexperiment + +const RuntimeFreegc = true +const RuntimeFreegcInt = 1 diff --git a/internal/go/src/goexperiment/exp_runtimesecret_off.go b/internal/go/src/goexperiment/exp_runtimesecret_off.go new file mode 100755 index 0000000..d203589 --- /dev/null +++ b/internal/go/src/goexperiment/exp_runtimesecret_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.runtimesecret + +package goexperiment + +const RuntimeSecret = false +const RuntimeSecretInt = 0 diff --git a/internal/go/src/goexperiment/exp_runtimesecret_on.go b/internal/go/src/goexperiment/exp_runtimesecret_on.go new file mode 100755 index 0000000..3788953 --- /dev/null +++ b/internal/go/src/goexperiment/exp_runtimesecret_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.runtimesecret + +package goexperiment + +const RuntimeSecret = true +const RuntimeSecretInt = 1 diff --git a/internal/go/src/goexperiment/exp_simd_off.go b/internal/go/src/goexperiment/exp_simd_off.go new file mode 100755 index 0000000..ebc40b3 --- /dev/null +++ b/internal/go/src/goexperiment/exp_simd_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.simd + +package goexperiment + +const SIMD = false +const SIMDInt = 0 diff --git a/internal/go/src/goexperiment/exp_simd_on.go b/internal/go/src/goexperiment/exp_simd_on.go new file mode 100755 index 0000000..137d1dd --- /dev/null +++ b/internal/go/src/goexperiment/exp_simd_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.simd + +package goexperiment + +const SIMD = true +const SIMDInt = 1 diff --git a/internal/go/src/goexperiment/exp_sizespecializedmalloc_off.go b/internal/go/src/goexperiment/exp_sizespecializedmalloc_off.go new file mode 100755 index 0000000..2642a12 --- /dev/null +++ b/internal/go/src/goexperiment/exp_sizespecializedmalloc_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.sizespecializedmalloc + +package goexperiment + +const SizeSpecializedMalloc = false +const SizeSpecializedMallocInt = 0 diff --git a/internal/go/src/goexperiment/exp_sizespecializedmalloc_on.go b/internal/go/src/goexperiment/exp_sizespecializedmalloc_on.go new file mode 100755 index 0000000..a135cc8 --- /dev/null +++ b/internal/go/src/goexperiment/exp_sizespecializedmalloc_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.sizespecializedmalloc + +package goexperiment + +const SizeSpecializedMalloc = true +const SizeSpecializedMallocInt = 1 diff --git a/internal/go/src/goexperiment/exp_staticlockranking_off.go b/internal/go/src/goexperiment/exp_staticlockranking_off.go new file mode 100755 index 0000000..5fafff2 --- /dev/null +++ b/internal/go/src/goexperiment/exp_staticlockranking_off.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build !goexperiment.staticlockranking + +package goexperiment + +const StaticLockRanking = false +const StaticLockRankingInt = 0 diff --git a/internal/go/src/goexperiment/exp_staticlockranking_on.go b/internal/go/src/goexperiment/exp_staticlockranking_on.go new file mode 100755 index 0000000..dfd32a8 --- /dev/null +++ b/internal/go/src/goexperiment/exp_staticlockranking_on.go @@ -0,0 +1,8 @@ +// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build goexperiment.staticlockranking + +package goexperiment + +const StaticLockRanking = true +const StaticLockRankingInt = 1 diff --git a/internal/go/src/goexperiment/flags.go b/internal/go/src/goexperiment/flags.go new file mode 100755 index 0000000..2cfb715 --- /dev/null +++ b/internal/go/src/goexperiment/flags.go @@ -0,0 +1,131 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package goexperiment implements support for toolchain experiments. +// +// Toolchain experiments are controlled by the GOEXPERIMENT +// environment variable. GOEXPERIMENT is a comma-separated list of +// experiment names. GOEXPERIMENT can be set at make.bash time, which +// sets the default experiments for binaries built with the tool +// chain; or it can be set at build time. GOEXPERIMENT can also be set +// to "none", which disables any experiments that were enabled at +// make.bash time. +// +// Experiments are exposed to the build in the following ways: +// +// - Build tag goexperiment.x is set if experiment x (lower case) is +// enabled. +// +// - For each experiment x (in camel case), this package contains a +// boolean constant x and an integer constant xInt. +// +// - In runtime assembly, the macro GOEXPERIMENT_x is defined if +// experiment x (lower case) is enabled. +// +// In the toolchain, the set of experiments enabled for the current +// build should be accessed via objabi.Experiment. +// +// The set of experiments is included in the output of [runtime.Version]() +// and "go version " if it differs from the default experiments. +// +// For the set of experiments supported by the current toolchain, see +// "go doc goexperiment.Flags". +// +// Note that this package defines the set of experiments (in [Flags]) +// and records the experiments that were enabled when the package +// was compiled (as boolean and integer constants). +// +// Note especially that this package does not itself change behavior +// at run time based on the GOEXPERIMENT variable. +// The code used in builds to interpret the GOEXPERIMENT variable +// is in the separate package [internal/buildcfg]. +package goexperiment + +//go:generate go run mkconsts.go + +// Flags is the set of experiments that can be enabled or disabled in +// the current toolchain. +// +// When specified in the GOEXPERIMENT environment variable or as build +// tags, experiments use the strings.ToLower of their field name. +// +// For the baseline experimental configuration, see +// [internal/buildcfg.Experiment]. +// +// If you change this struct definition, run "go generate". +type Flags struct { + FieldTrack bool + PreemptibleLoops bool + StaticLockRanking bool + BoringCrypto bool + + // Regabi is split into several sub-experiments that can be + // enabled individually. Not all combinations work. + // The "regabi" GOEXPERIMENT is an alias for all "working" + // subexperiments. + + // RegabiWrappers enables ABI wrappers for calling between + // ABI0 and ABIInternal functions. Without this, the ABIs are + // assumed to be identical so cross-ABI calls are direct. + RegabiWrappers bool + // RegabiArgs enables register arguments/results in all + // compiled Go functions. + // + // Requires wrappers (to do ABI translation), and reflect (so + // reflection calls use registers). + RegabiArgs bool + + // HeapMinimum512KiB reduces the minimum heap size to 512 KiB. + // + // This was originally reduced as part of PacerRedesign, but + // has been broken out to its own experiment that is disabled + // by default. + HeapMinimum512KiB bool + + // Arenas causes the "arena" standard library package to be visible + // to the outside world. + Arenas bool + + // CgoCheck2 enables an expensive cgo rule checker. + // When this experiment is enabled, cgo rule checks occur regardless + // of the GODEBUG=cgocheck setting provided at runtime. + CgoCheck2 bool + + // LoopVar changes loop semantics so that each iteration gets its own + // copy of the iteration variable. + LoopVar bool + + // NewInliner enables a new+improved version of the function + // inlining phase within the Go compiler. + NewInliner bool + + // Dwarf5 enables DWARF version 5 debug info generation. + Dwarf5 bool + + // JSONv2 enables the json/v2 package. + JSONv2 bool + + // GreenTeaGC enables the Green Tea GC implementation. + GreenTeaGC bool + + // RandomizedHeapBase enables heap base address randomization on 64-bit + // platforms. + RandomizedHeapBase64 bool + + // RuntimeFreegc enables the runtime to free and reuse memory more eagerly in some circumstances with compiler help. + RuntimeFreegc bool + + // SizeSpecializedMalloc enables malloc implementations that are specialized per size class. + SizeSpecializedMalloc bool + + // GoroutineLeakProfile enables the collection of goroutine leak profiles. + GoroutineLeakProfile bool + + // SIMD enables the simd package and the compiler's handling + // of SIMD intrinsics. + SIMD bool + + // RuntimeSecret enables the runtime/secret package. + RuntimeSecret bool +} diff --git a/internal/go/src/goexperiment/mkconsts.go b/internal/go/src/goexperiment/mkconsts.go new file mode 100755 index 0000000..7f14392 --- /dev/null +++ b/internal/go/src/goexperiment/mkconsts.go @@ -0,0 +1,72 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +// mkconsts generates const definition files for each GOEXPERIMENT. +package main + +import ( + "bytes" + "fmt" + "loov.dev/lensm/internal/go/src/goexperiment" + "log" + "os" + "reflect" + "strings" +) + +func main() { + // Delete existing experiment constant files. + ents, err := os.ReadDir(".") + if err != nil { + log.Fatal(err) + } + for _, ent := range ents { + name := ent.Name() + if !strings.HasPrefix(name, "exp_") { + continue + } + // Check that this is definitely a generated file. + data, err := os.ReadFile(name) + if err != nil { + log.Fatalf("reading %s: %v", name, err) + } + if !bytes.Contains(data, []byte("Code generated by mkconsts")) { + log.Fatalf("%s: expected generated file", name) + } + if err := os.Remove(name); err != nil { + log.Fatal(err) + } + } + + // Generate new experiment constant files. + rt := reflect.TypeOf(&goexperiment.Flags{}).Elem() + for i := 0; i < rt.NumField(); i++ { + f := rt.Field(i).Name + buildTag := "goexperiment." + strings.ToLower(f) + for _, val := range []bool{false, true} { + name := fmt.Sprintf("exp_%s_%s.go", strings.ToLower(f), pick(val, "off", "on")) + data := fmt.Sprintf(`// Code generated by mkconsts.go. DO NOT EDIT. + +//go:build %s%s + +package goexperiment + +const %s = %v +const %sInt = %s +`, pick(val, "!", ""), buildTag, f, val, f, pick(val, "0", "1")) + if err := os.WriteFile(name, []byte(data), 0666); err != nil { + log.Fatalf("writing %s: %v", name, err) + } + } + } +} + +func pick(v bool, f, t string) string { + if v { + return t + } + return f +} diff --git a/internal/go/src/goobj/builtin.go b/internal/go/src/goobj/builtin.go new file mode 100755 index 0000000..cc28ef9 --- /dev/null +++ b/internal/go/src/goobj/builtin.go @@ -0,0 +1,47 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goobj + +import "loov.dev/lensm/internal/go/src/buildcfg" + +// Builtin (compiler-generated) function references appear +// frequently. We assign special indices for them, so they +// don't need to be referenced by name. + +// NBuiltin returns the number of listed builtin +// symbols. +func NBuiltin() int { + return len(builtins) +} + +// BuiltinName returns the name and ABI of the i-th +// builtin symbol. +func BuiltinName(i int) (string, int) { + return builtins[i].name, builtins[i].abi +} + +// BuiltinIdx returns the index of the builtin with the +// given name and abi, or -1 if it is not a builtin. +func BuiltinIdx(name string, abi int) int { + i, ok := builtinMap[name] + if !ok { + return -1 + } + if buildcfg.Experiment.RegabiWrappers && builtins[i].abi != abi { + return -1 + } + return i +} + +//go:generate go run mkbuiltin.go + +var builtinMap map[string]int + +func init() { + builtinMap = make(map[string]int, len(builtins)) + for i, b := range builtins { + builtinMap[b.name] = i + } +} diff --git a/internal/go/src/goobj/builtinlist.go b/internal/go/src/goobj/builtinlist.go new file mode 100755 index 0000000..918ade1 --- /dev/null +++ b/internal/go/src/goobj/builtinlist.go @@ -0,0 +1,275 @@ +// Code generated by mkbuiltin.go. DO NOT EDIT. + +package goobj + +var builtins = [...]struct { + name string + abi int +}{ + {"runtime.newobject", 1}, + {"runtime.mallocgc", 1}, + {"runtime.panicdivide", 1}, + {"runtime.panicshift", 1}, + {"runtime.panicmakeslicelen", 1}, + {"runtime.panicmakeslicecap", 1}, + {"runtime.throwinit", 1}, + {"runtime.panicwrap", 1}, + {"runtime.gopanic", 1}, + {"runtime.gorecover", 1}, + {"runtime.goschedguarded", 1}, + {"runtime.goPanicIndex", 1}, + {"runtime.goPanicIndexU", 1}, + {"runtime.goPanicSliceAlen", 1}, + {"runtime.goPanicSliceAlenU", 1}, + {"runtime.goPanicSliceAcap", 1}, + {"runtime.goPanicSliceAcapU", 1}, + {"runtime.goPanicSliceB", 1}, + {"runtime.goPanicSliceBU", 1}, + {"runtime.goPanicSlice3Alen", 1}, + {"runtime.goPanicSlice3AlenU", 1}, + {"runtime.goPanicSlice3Acap", 1}, + {"runtime.goPanicSlice3AcapU", 1}, + {"runtime.goPanicSlice3B", 1}, + {"runtime.goPanicSlice3BU", 1}, + {"runtime.goPanicSlice3C", 1}, + {"runtime.goPanicSlice3CU", 1}, + {"runtime.goPanicSliceConvert", 1}, + {"runtime.printbool", 1}, + {"runtime.printfloat64", 1}, + {"runtime.printfloat32", 1}, + {"runtime.printint", 1}, + {"runtime.printhex", 1}, + {"runtime.printuint", 1}, + {"runtime.printcomplex128", 1}, + {"runtime.printcomplex64", 1}, + {"runtime.printstring", 1}, + {"runtime.printquoted", 1}, + {"runtime.printpointer", 1}, + {"runtime.printuintptr", 1}, + {"runtime.printiface", 1}, + {"runtime.printeface", 1}, + {"runtime.printslice", 1}, + {"runtime.printnl", 1}, + {"runtime.printsp", 1}, + {"runtime.printlock", 1}, + {"runtime.printunlock", 1}, + {"runtime.concatstring2", 1}, + {"runtime.concatstring3", 1}, + {"runtime.concatstring4", 1}, + {"runtime.concatstring5", 1}, + {"runtime.concatstrings", 1}, + {"runtime.concatbyte2", 1}, + {"runtime.concatbyte3", 1}, + {"runtime.concatbyte4", 1}, + {"runtime.concatbyte5", 1}, + {"runtime.concatbytes", 1}, + {"runtime.cmpstring", 1}, + {"runtime.intstring", 1}, + {"runtime.slicebytetostring", 1}, + {"runtime.slicebytetostringtmp", 1}, + {"runtime.slicerunetostring", 1}, + {"runtime.stringtoslicebyte", 1}, + {"runtime.stringtoslicerune", 1}, + {"runtime.slicecopy", 1}, + {"runtime.decoderune", 1}, + {"runtime.countrunes", 1}, + {"runtime.convT", 1}, + {"runtime.convTnoptr", 1}, + {"runtime.convT16", 1}, + {"runtime.convT32", 1}, + {"runtime.convT64", 1}, + {"runtime.convTstring", 1}, + {"runtime.convTslice", 1}, + {"runtime.assertE2I", 1}, + {"runtime.assertE2I2", 1}, + {"runtime.panicdottypeE", 1}, + {"runtime.panicdottypeI", 1}, + {"runtime.panicnildottype", 1}, + {"runtime.typeAssert", 1}, + {"runtime.interfaceSwitch", 1}, + {"runtime.ifaceeq", 1}, + {"runtime.efaceeq", 1}, + {"runtime.panicrangestate", 1}, + {"runtime.deferrangefunc", 1}, + {"runtime.rand", 1}, + {"runtime.rand32", 1}, + {"runtime.makemap64", 1}, + {"runtime.makemap", 1}, + {"runtime.makemap_small", 1}, + {"runtime.mapaccess1", 1}, + {"runtime.mapaccess1_fast32", 1}, + {"runtime.mapaccess1_fast64", 1}, + {"runtime.mapaccess1_faststr", 1}, + {"runtime.mapaccess1_fat", 1}, + {"runtime.mapaccess2", 1}, + {"runtime.mapaccess2_fast32", 1}, + {"runtime.mapaccess2_fast64", 1}, + {"runtime.mapaccess2_faststr", 1}, + {"runtime.mapaccess2_fat", 1}, + {"runtime.mapassign", 1}, + {"runtime.mapassign_fast32", 1}, + {"runtime.mapassign_fast32ptr", 1}, + {"runtime.mapassign_fast64", 1}, + {"runtime.mapassign_fast64ptr", 1}, + {"runtime.mapassign_faststr", 1}, + {"runtime.mapIterStart", 1}, + {"runtime.mapdelete", 1}, + {"runtime.mapdelete_fast32", 1}, + {"runtime.mapdelete_fast64", 1}, + {"runtime.mapdelete_faststr", 1}, + {"runtime.mapIterNext", 1}, + {"runtime.mapclear", 1}, + {"runtime.makechan64", 1}, + {"runtime.makechan", 1}, + {"runtime.chanrecv1", 1}, + {"runtime.chanrecv2", 1}, + {"runtime.chansend1", 1}, + {"runtime.closechan", 1}, + {"runtime.chanlen", 1}, + {"runtime.chancap", 1}, + {"runtime.writeBarrier", 0}, + {"runtime.typedmemmove", 1}, + {"runtime.typedmemclr", 1}, + {"runtime.typedslicecopy", 1}, + {"runtime.selectnbsend", 1}, + {"runtime.selectnbrecv", 1}, + {"runtime.selectsetpc", 1}, + {"runtime.selectgo", 1}, + {"runtime.block", 1}, + {"runtime.makeslice", 1}, + {"runtime.makeslice64", 1}, + {"runtime.makeslicecopy", 1}, + {"runtime.growslice", 1}, + {"runtime.unsafeslicecheckptr", 1}, + {"runtime.panicunsafeslicelen", 1}, + {"runtime.panicunsafeslicenilptr", 1}, + {"runtime.unsafestringcheckptr", 1}, + {"runtime.panicunsafestringlen", 1}, + {"runtime.panicunsafestringnilptr", 1}, + {"runtime.memmove", 1}, + {"runtime.memclrNoHeapPointers", 1}, + {"runtime.memclrHasPointers", 1}, + {"runtime.memequal", 1}, + {"runtime.memequal0", 1}, + {"runtime.memequal8", 1}, + {"runtime.memequal16", 1}, + {"runtime.memequal32", 1}, + {"runtime.memequal64", 1}, + {"runtime.memequal128", 1}, + {"runtime.f32equal", 1}, + {"runtime.f64equal", 1}, + {"runtime.c64equal", 1}, + {"runtime.c128equal", 1}, + {"runtime.strequal", 1}, + {"runtime.interequal", 1}, + {"runtime.nilinterequal", 1}, + {"runtime.memhash", 1}, + {"runtime.memhash0", 1}, + {"runtime.memhash8", 1}, + {"runtime.memhash16", 1}, + {"runtime.memhash32", 1}, + {"runtime.memhash64", 1}, + {"runtime.memhash128", 1}, + {"runtime.f32hash", 1}, + {"runtime.f64hash", 1}, + {"runtime.c64hash", 1}, + {"runtime.c128hash", 1}, + {"runtime.strhash", 1}, + {"runtime.interhash", 1}, + {"runtime.nilinterhash", 1}, + {"runtime.int64div", 1}, + {"runtime.uint64div", 1}, + {"runtime.int64mod", 1}, + {"runtime.uint64mod", 1}, + {"runtime.float64toint64", 1}, + {"runtime.float64touint64", 1}, + {"runtime.float64touint32", 1}, + {"runtime.int64tofloat64", 1}, + {"runtime.int64tofloat32", 1}, + {"runtime.uint64tofloat64", 1}, + {"runtime.uint64tofloat32", 1}, + {"runtime.uint32tofloat64", 1}, + {"runtime.complex128div", 1}, + {"runtime.racefuncenter", 1}, + {"runtime.racefuncexit", 1}, + {"runtime.raceread", 1}, + {"runtime.racewrite", 1}, + {"runtime.racereadrange", 1}, + {"runtime.racewriterange", 1}, + {"runtime.msanread", 1}, + {"runtime.msanwrite", 1}, + {"runtime.msanmove", 1}, + {"runtime.asanread", 1}, + {"runtime.asanwrite", 1}, + {"runtime.checkptrAlignment", 1}, + {"runtime.checkptrArithmetic", 1}, + {"runtime.libfuzzerTraceCmp1", 1}, + {"runtime.libfuzzerTraceCmp2", 1}, + {"runtime.libfuzzerTraceCmp4", 1}, + {"runtime.libfuzzerTraceCmp8", 1}, + {"runtime.libfuzzerTraceConstCmp1", 1}, + {"runtime.libfuzzerTraceConstCmp2", 1}, + {"runtime.libfuzzerTraceConstCmp4", 1}, + {"runtime.libfuzzerTraceConstCmp8", 1}, + {"runtime.libfuzzerHookStrCmp", 1}, + {"runtime.libfuzzerHookEqualFold", 1}, + {"runtime.addCovMeta", 1}, + {"runtime.x86HasPOPCNT", 0}, + {"runtime.x86HasSSE41", 0}, + {"runtime.x86HasFMA", 0}, + {"runtime.armHasVFPv4", 0}, + {"runtime.arm64HasATOMICS", 0}, + {"runtime.loong64HasLAMCAS", 0}, + {"runtime.loong64HasLAM_BH", 0}, + {"runtime.loong64HasLSX", 0}, + {"runtime.riscv64HasZbb", 0}, + {"runtime.asanregisterglobals", 1}, + {"runtime.deferproc", 1}, + {"runtime.deferprocStack", 1}, + {"runtime.deferreturn", 1}, + {"runtime.newproc", 1}, + {"runtime.panicoverflow", 1}, + {"runtime.sigpanic", 1}, + {"runtime.gcWriteBarrier", 1}, + {"runtime.duffzero", 1}, + {"runtime.duffcopy", 1}, + {"runtime.morestack", 0}, + {"runtime.morestackc", 0}, + {"runtime.morestack_noctxt", 0}, + {"type:int8", 0}, + {"type:*int8", 0}, + {"type:uint8", 0}, + {"type:*uint8", 0}, + {"type:int16", 0}, + {"type:*int16", 0}, + {"type:uint16", 0}, + {"type:*uint16", 0}, + {"type:int32", 0}, + {"type:*int32", 0}, + {"type:uint32", 0}, + {"type:*uint32", 0}, + {"type:int64", 0}, + {"type:*int64", 0}, + {"type:uint64", 0}, + {"type:*uint64", 0}, + {"type:float32", 0}, + {"type:*float32", 0}, + {"type:float64", 0}, + {"type:*float64", 0}, + {"type:complex64", 0}, + {"type:*complex64", 0}, + {"type:complex128", 0}, + {"type:*complex128", 0}, + {"type:unsafe.Pointer", 0}, + {"type:*unsafe.Pointer", 0}, + {"type:uintptr", 0}, + {"type:*uintptr", 0}, + {"type:bool", 0}, + {"type:*bool", 0}, + {"type:string", 0}, + {"type:*string", 0}, + {"type:error", 0}, + {"type:*error", 0}, + {"type:func(error) string", 0}, + {"type:*func(error) string", 0}, +} diff --git a/internal/go/src/goobj/funcinfo.go b/internal/go/src/goobj/funcinfo.go new file mode 100755 index 0000000..800aac5 --- /dev/null +++ b/internal/go/src/goobj/funcinfo.go @@ -0,0 +1,145 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package goobj + +import ( + "bytes" + "encoding/binary" + "loov.dev/lensm/internal/go/src/abi" +) + +// CUFileIndex is used to index the filenames that are stored in the +// per-package/per-CU FileList. +type CUFileIndex uint32 + +// FuncInfo is serialized as a symbol (aux symbol). The symbol data is +// the binary encoding of the struct below. +type FuncInfo struct { + Args uint32 + Locals uint32 + FuncID abi.FuncID + FuncFlag abi.FuncFlag + StartLine int32 + File []CUFileIndex + InlTree []InlTreeNode +} + +func (a *FuncInfo) Write(w *bytes.Buffer) { + writeUint8 := func(x uint8) { + w.WriteByte(x) + } + var b [4]byte + writeUint32 := func(x uint32) { + binary.LittleEndian.PutUint32(b[:], x) + w.Write(b[:]) + } + + writeUint32(a.Args) + writeUint32(a.Locals) + writeUint8(uint8(a.FuncID)) + writeUint8(uint8(a.FuncFlag)) + writeUint8(0) // pad to uint32 boundary + writeUint8(0) + writeUint32(uint32(a.StartLine)) + + writeUint32(uint32(len(a.File))) + for _, f := range a.File { + writeUint32(uint32(f)) + } + writeUint32(uint32(len(a.InlTree))) + for i := range a.InlTree { + a.InlTree[i].Write(w) + } +} + +// FuncInfoLengths is a cache containing a roadmap of offsets and +// lengths for things within a serialized FuncInfo. Each length field +// stores the number of items (e.g. files, inltree nodes, etc), and the +// corresponding "off" field stores the byte offset of the start of +// the items in question. +type FuncInfoLengths struct { + NumFile uint32 + FileOff uint32 + NumInlTree uint32 + InlTreeOff uint32 + Initialized bool +} + +func (*FuncInfo) ReadFuncInfoLengths(b []byte) FuncInfoLengths { + var result FuncInfoLengths + + // Offset to the number of the file table. This value is determined by counting + // the number of bytes until we write funcdataoff to the file. + const numfileOff = 16 + result.NumFile = binary.LittleEndian.Uint32(b[numfileOff:]) + result.FileOff = numfileOff + 4 + + numinltreeOff := result.FileOff + 4*result.NumFile + result.NumInlTree = binary.LittleEndian.Uint32(b[numinltreeOff:]) + result.InlTreeOff = numinltreeOff + 4 + + result.Initialized = true + + return result +} + +func (*FuncInfo) ReadArgs(b []byte) uint32 { return binary.LittleEndian.Uint32(b) } + +func (*FuncInfo) ReadLocals(b []byte) uint32 { return binary.LittleEndian.Uint32(b[4:]) } + +func (*FuncInfo) ReadFuncID(b []byte) abi.FuncID { return abi.FuncID(b[8]) } + +func (*FuncInfo) ReadFuncFlag(b []byte) abi.FuncFlag { return abi.FuncFlag(b[9]) } + +func (*FuncInfo) ReadStartLine(b []byte) int32 { return int32(binary.LittleEndian.Uint32(b[12:])) } + +func (*FuncInfo) ReadFile(b []byte, filesoff uint32, k uint32) CUFileIndex { + return CUFileIndex(binary.LittleEndian.Uint32(b[filesoff+4*k:])) +} + +func (*FuncInfo) ReadInlTree(b []byte, inltreeoff uint32, k uint32) InlTreeNode { + const inlTreeNodeSize = 4 * 6 + var result InlTreeNode + result.Read(b[inltreeoff+k*inlTreeNodeSize:]) + return result +} + +// InlTreeNode is the serialized form of FileInfo.InlTree. +type InlTreeNode struct { + Parent int32 + File CUFileIndex + Line int32 + Func SymRef + ParentPC int32 +} + +func (inl *InlTreeNode) Write(w *bytes.Buffer) { + var b [4]byte + writeUint32 := func(x uint32) { + binary.LittleEndian.PutUint32(b[:], x) + w.Write(b[:]) + } + writeUint32(uint32(inl.Parent)) + writeUint32(uint32(inl.File)) + writeUint32(uint32(inl.Line)) + writeUint32(inl.Func.PkgIdx) + writeUint32(inl.Func.SymIdx) + writeUint32(uint32(inl.ParentPC)) +} + +// Read an InlTreeNode from b, return the remaining bytes. +func (inl *InlTreeNode) Read(b []byte) []byte { + readUint32 := func() uint32 { + x := binary.LittleEndian.Uint32(b) + b = b[4:] + return x + } + inl.Parent = int32(readUint32()) + inl.File = CUFileIndex(readUint32()) + inl.Line = int32(readUint32()) + inl.Func = SymRef{readUint32(), readUint32()} + inl.ParentPC = int32(readUint32()) + return b +} diff --git a/internal/go/src/goobj/mkbuiltin.go b/internal/go/src/goobj/mkbuiltin.go new file mode 100755 index 0000000..6d1e7bb --- /dev/null +++ b/internal/go/src/goobj/mkbuiltin.go @@ -0,0 +1,160 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build ignore + +// Generate builtinlist.go from cmd/compile/internal/typecheck/_builtin/runtime.go. + +package main + +import ( + "bytes" + "flag" + "fmt" + "go/ast" + "go/format" + "go/parser" + "go/token" + "io" + "log" + "os" + "path/filepath" + "strings" +) + +var stdout = flag.Bool("stdout", false, "write to stdout instead of builtinlist.go") + +func main() { + flag.Parse() + + var b bytes.Buffer + fmt.Fprintln(&b, "// Code generated by mkbuiltin.go. DO NOT EDIT.") + fmt.Fprintln(&b) + fmt.Fprintln(&b, "package goobj") + + mkbuiltin(&b) + + out, err := format.Source(b.Bytes()) + if err != nil { + log.Fatal(err) + } + if *stdout { + _, err = os.Stdout.Write(out) + } else { + err = os.WriteFile("builtinlist.go", out, 0666) + } + if err != nil { + log.Fatal(err) + } +} + +func mkbuiltin(w io.Writer) { + pkg := "runtime" + fset := token.NewFileSet() + path := filepath.Join("..", "..", "compile", "internal", "typecheck", "_builtin", "runtime.go") + f, err := parser.ParseFile(fset, path, nil, 0) + if err != nil { + log.Fatal(err) + } + + decls := make(map[string]bool) + + fmt.Fprintf(w, "var builtins = [...]struct{ name string; abi int }{\n") + for _, decl := range f.Decls { + switch decl := decl.(type) { + case *ast.FuncDecl: + if decl.Recv != nil { + log.Fatal("methods unsupported") + } + if decl.Body != nil { + log.Fatal("unexpected function body") + } + declName := pkg + "." + decl.Name.Name + decls[declName] = true + fmt.Fprintf(w, "{%q, 1},\n", declName) // functions are ABIInternal (1) + case *ast.GenDecl: + if decl.Tok == token.IMPORT { + continue + } + if decl.Tok != token.VAR { + log.Fatal("unhandled declaration kind: ", decl.Tok) + } + for _, spec := range decl.Specs { + spec := spec.(*ast.ValueSpec) + if len(spec.Values) != 0 { + log.Fatal("unexpected values") + } + for _, name := range spec.Names { + declName := pkg + "." + name.Name + decls[declName] = true + fmt.Fprintf(w, "{%q, 0},\n", declName) // variables are ABI0 + } + } + default: + log.Fatal("unhandled decl type: ", decl) + } + } + + // The list above only contains ones that are used by the frontend. + // The backend may create more references of builtin functions. + // We also want to include predefined types. + // Add them. + extras := append(fextras[:], enumerateBasicTypes()...) + for _, b := range extras { + prefix := "" + if !strings.HasPrefix(b.name, "type:") { + prefix = pkg + "." + } + name := prefix + b.name + if decls[name] { + log.Fatalf("%q already added -- mkbuiltin.go out of sync?", name) + } + fmt.Fprintf(w, "{%q, %d},\n", name, b.abi) + } + fmt.Fprintln(w, "}") +} + +// enumerateBasicTypes returns the symbol names for basic types that are +// defined in the runtime and referenced in other packages. +// Needs to be kept in sync with reflect.go:WriteBasicTypes() and +// reflect.go:writeType() in the compiler. +func enumerateBasicTypes() []extra { + names := [...]string{ + "int8", "uint8", "int16", "uint16", + "int32", "uint32", "int64", "uint64", + "float32", "float64", "complex64", "complex128", + "unsafe.Pointer", "uintptr", "bool", "string", "error", + "func(error) string"} + result := []extra{} + for _, n := range names { + result = append(result, extra{"type:" + n, 0}) + result = append(result, extra{"type:*" + n, 0}) + } + return result +} + +type extra struct { + name string + abi int +} + +var fextras = [...]extra{ + // compiler frontend inserted calls (sysfunc) + {"deferproc", 1}, + {"deferprocStack", 1}, + {"deferreturn", 1}, + {"newproc", 1}, + {"panicoverflow", 1}, + {"sigpanic", 1}, + + // compiler backend inserted calls + {"gcWriteBarrier", 1}, + {"duffzero", 1}, + {"duffcopy", 1}, + + // assembler backend inserted calls + {"morestack", 0}, // asm function, ABI0 + {"morestackc", 0}, // asm function, ABI0 + {"morestack_noctxt", 0}, // asm function, ABI0 +} diff --git a/internal/go/src/goobj/objfile.go b/internal/go/src/goobj/objfile.go new file mode 100755 index 0000000..a92e5c8 --- /dev/null +++ b/internal/go/src/goobj/objfile.go @@ -0,0 +1,874 @@ +// Copyright 2019 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This package defines the Go object file format, and provide "low-level" functions +// for reading and writing object files. + +// The object file is understood by the compiler, assembler, linker, and tools. They +// have "high level" code that operates on object files, handling application-specific +// logics, and use this package for the actual reading and writing. Specifically, the +// code below: +// +// - cmd/internal/obj/objfile.go (used by cmd/asm and cmd/compile) +// - cmd/internal/objfile/goobj.go (used cmd/nm, cmd/objdump) +// - cmd/link/internal/loader package (used by cmd/link) +// +// If the object file format changes, they may (or may not) need to change. + +package goobj + +import ( + "loov.dev/lensm/internal/go/src/bio" + "encoding/binary" + "errors" + "fmt" + "unsafe" +) + +// New object file format. +// +// Header struct { +// Magic [...]byte // "\x00go120ld" +// Fingerprint [8]byte +// Flags uint32 +// Offsets [...]uint32 // byte offset of each block below +// } +// +// Strings [...]struct { +// Data [...]byte +// } +// +// Autolib [...]struct { // imported packages (for file loading) +// Pkg string +// Fingerprint [8]byte +// } +// +// PkgIndex [...]string // referenced packages by index +// +// Files [...]string +// +// SymbolDefs [...]struct { +// Name string +// ABI uint16 +// Type uint8 +// Flag uint8 +// Flag2 uint8 +// Size uint32 +// Align uint32 +// } +// Hashed64Defs [...]struct { // short hashed (content-addressable) symbol definitions +// ... // same as SymbolDefs +// } +// HashedDefs [...]struct { // hashed (content-addressable) symbol definitions +// ... // same as SymbolDefs +// } +// NonPkgDefs [...]struct { // non-pkg symbol definitions +// ... // same as SymbolDefs +// } +// NonPkgRefs [...]struct { // non-pkg symbol references +// ... // same as SymbolDefs +// } +// +// RefFlags [...]struct { // referenced symbol flags +// Sym symRef +// Flag uint8 +// Flag2 uint8 +// } +// +// Hash64 [...][8]byte +// Hash [...][N]byte +// +// RelocIndex [...]uint32 // index to Relocs +// AuxIndex [...]uint32 // index to Aux +// DataIndex [...]uint32 // offset to Data +// +// Relocs [...]struct { +// Off int32 +// Size uint8 +// Type uint16 +// Add int64 +// Sym symRef +// } +// +// Aux [...]struct { +// Type uint8 +// Sym symRef +// } +// +// Data [...]byte +// +// // blocks only used by tools (objdump, nm) +// +// RefNames [...]struct { // referenced symbol names +// Sym symRef +// Name string +// // TODO: include ABI version as well? +// } +// +// string is encoded as is a uint32 length followed by a uint32 offset +// that points to the corresponding string bytes. +// +// symRef is struct { PkgIdx, SymIdx uint32 }. +// +// Slice type (e.g. []symRef) is encoded as a length prefix (uint32) +// followed by that number of elements. +// +// The types below correspond to the encoded data structure in the +// object file. + +// Symbol indexing. +// +// Each symbol is referenced with a pair of indices, { PkgIdx, SymIdx }, +// as the symRef struct above. +// +// PkgIdx is either a predeclared index (see PkgIdxNone below) or +// an index of an imported package. For the latter case, PkgIdx is the +// index of the package in the PkgIndex array. 0 is an invalid index. +// +// SymIdx is the index of the symbol in the given package. +// - If PkgIdx is PkgIdxSelf, SymIdx is the index of the symbol in the +// SymbolDefs array. +// - If PkgIdx is PkgIdxHashed64, SymIdx is the index of the symbol in the +// Hashed64Defs array. +// - If PkgIdx is PkgIdxHashed, SymIdx is the index of the symbol in the +// HashedDefs array. +// - If PkgIdx is PkgIdxNone, SymIdx is the index of the symbol in the +// NonPkgDefs array (could naturally overflow to NonPkgRefs array). +// - Otherwise, SymIdx is the index of the symbol in some other package's +// SymbolDefs array. +// +// {0, 0} represents a nil symbol. Otherwise PkgIdx should not be 0. +// +// Hash contains the content hashes of content-addressable symbols, of +// which PkgIdx is PkgIdxHashed, in the same order of HashedDefs array. +// Hash64 is similar, for PkgIdxHashed64 symbols. +// +// RelocIndex, AuxIndex, and DataIndex contains indices/offsets to +// Relocs/Aux/Data blocks, one element per symbol, first for all the +// defined symbols, then all the defined hashed and non-package symbols, +// in the same order of SymbolDefs/Hashed64Defs/HashedDefs/NonPkgDefs +// arrays. For N total defined symbols, the array is of length N+1. The +// last element is the total number of relocations (aux symbols, data +// blocks, etc.). +// +// They can be accessed by index. For the i-th symbol, its relocations +// are the RelocIndex[i]-th (inclusive) to RelocIndex[i+1]-th (exclusive) +// elements in the Relocs array. Aux/Data are likewise. (The index is +// 0-based.) + +// Auxiliary symbols. +// +// Each symbol may (or may not) be associated with a number of auxiliary +// symbols. They are described in the Aux block. See Aux struct below. +// Currently a symbol's Gotype, FuncInfo, and associated DWARF symbols +// are auxiliary symbols. + +const stringRefSize = 8 // two uint32s + +type FingerprintType [8]byte + +func (fp FingerprintType) IsZero() bool { return fp == FingerprintType{} } + +// Package Index. +const ( + PkgIdxNone = (1<<31 - 1) - iota // Non-package symbols + PkgIdxHashed64 // Short hashed (content-addressable) symbols + PkgIdxHashed // Hashed (content-addressable) symbols + PkgIdxBuiltin // Predefined runtime symbols (ex: runtime.newobject) + PkgIdxSelf // Symbols defined in the current package + PkgIdxSpecial = PkgIdxSelf // Indices above it has special meanings + PkgIdxInvalid = 0 + // The index of other referenced packages starts from 1. +) + +// Blocks +const ( + BlkAutolib = iota + BlkPkgIdx + BlkFile + BlkSymdef + BlkHashed64def + BlkHasheddef + BlkNonpkgdef + BlkNonpkgref + BlkRefFlags + BlkHash64 + BlkHash + BlkRelocIdx + BlkAuxIdx + BlkDataIdx + BlkReloc + BlkAux + BlkData + BlkRefName + BlkEnd + NBlk +) + +// File header. +// TODO: probably no need to export this. +type Header struct { + Magic string + Fingerprint FingerprintType + Flags uint32 + Offsets [NBlk]uint32 +} + +const Magic = "\x00go120ld" + +func (h *Header) Write(w *Writer) { + w.RawString(h.Magic) + w.Bytes(h.Fingerprint[:]) + w.Uint32(h.Flags) + for _, x := range h.Offsets { + w.Uint32(x) + } +} + +func (h *Header) Read(r *Reader) error { + b := r.BytesAt(0, len(Magic)) + h.Magic = string(b) + if h.Magic != Magic { + return errors.New("wrong magic, not a Go object file") + } + off := uint32(len(h.Magic)) + copy(h.Fingerprint[:], r.BytesAt(off, len(h.Fingerprint))) + off += 8 + h.Flags = r.uint32At(off) + off += 4 + for i := range h.Offsets { + h.Offsets[i] = r.uint32At(off) + off += 4 + } + return nil +} + +func (h *Header) Size() int { + return len(h.Magic) + len(h.Fingerprint) + 4 + 4*len(h.Offsets) +} + +// Autolib +type ImportedPkg struct { + Pkg string + Fingerprint FingerprintType +} + +const importedPkgSize = stringRefSize + 8 + +func (p *ImportedPkg) Write(w *Writer) { + w.StringRef(p.Pkg) + w.Bytes(p.Fingerprint[:]) +} + +// Symbol definition. +// +// Serialized format: +// +// Sym struct { +// Name string +// ABI uint16 +// Type uint8 +// Flag uint8 +// Flag2 uint8 +// Siz uint32 +// Align uint32 +// } +type Sym [SymSize]byte + +const SymSize = stringRefSize + 2 + 1 + 1 + 1 + 4 + 4 + +const SymABIstatic = ^uint16(0) + +const ( + ObjFlagShared = 1 << iota // this object is built with -shared + _ // was ObjFlagNeedNameExpansion + ObjFlagFromAssembly // object is from asm src, not go + ObjFlagUnlinkable // unlinkable package (linker will emit an error) + ObjFlagStd // standard library package +) + +// Sym.Flag +const ( + SymFlagDupok = 1 << iota + SymFlagLocal + SymFlagTypelink + SymFlagLeaf + SymFlagNoSplit + SymFlagReflectMethod + SymFlagGoType +) + +// Sym.Flag2 +const ( + SymFlagUsedInIface = 1 << iota + SymFlagItab + SymFlagDict + SymFlagPkgInit + SymFlagLinkname + SymFlagABIWrapper + SymFlagWasmExport +) + +// Returns the length of the name of the symbol. +func (s *Sym) NameLen(r *Reader) int { + return int(binary.LittleEndian.Uint32(s[:])) +} + +func (s *Sym) Name(r *Reader) string { + len := binary.LittleEndian.Uint32(s[:]) + off := binary.LittleEndian.Uint32(s[4:]) + return r.StringAt(off, len) +} + +func (s *Sym) ABI() uint16 { return binary.LittleEndian.Uint16(s[8:]) } +func (s *Sym) Type() uint8 { return s[10] } +func (s *Sym) Flag() uint8 { return s[11] } +func (s *Sym) Flag2() uint8 { return s[12] } +func (s *Sym) Siz() uint32 { return binary.LittleEndian.Uint32(s[13:]) } +func (s *Sym) Align() uint32 { return binary.LittleEndian.Uint32(s[17:]) } + +func (s *Sym) Dupok() bool { return s.Flag()&SymFlagDupok != 0 } +func (s *Sym) Local() bool { return s.Flag()&SymFlagLocal != 0 } +func (s *Sym) Typelink() bool { return s.Flag()&SymFlagTypelink != 0 } +func (s *Sym) Leaf() bool { return s.Flag()&SymFlagLeaf != 0 } +func (s *Sym) NoSplit() bool { return s.Flag()&SymFlagNoSplit != 0 } +func (s *Sym) ReflectMethod() bool { return s.Flag()&SymFlagReflectMethod != 0 } +func (s *Sym) IsGoType() bool { return s.Flag()&SymFlagGoType != 0 } +func (s *Sym) UsedInIface() bool { return s.Flag2()&SymFlagUsedInIface != 0 } +func (s *Sym) IsItab() bool { return s.Flag2()&SymFlagItab != 0 } +func (s *Sym) IsDict() bool { return s.Flag2()&SymFlagDict != 0 } +func (s *Sym) IsPkgInit() bool { return s.Flag2()&SymFlagPkgInit != 0 } +func (s *Sym) IsLinkname() bool { return s.Flag2()&SymFlagLinkname != 0 } +func (s *Sym) ABIWrapper() bool { return s.Flag2()&SymFlagABIWrapper != 0 } +func (s *Sym) WasmExport() bool { return s.Flag2()&SymFlagWasmExport != 0 } + +func (s *Sym) SetName(x string, w *Writer) { + binary.LittleEndian.PutUint32(s[:], uint32(len(x))) + binary.LittleEndian.PutUint32(s[4:], w.stringOff(x)) +} + +func (s *Sym) SetABI(x uint16) { binary.LittleEndian.PutUint16(s[8:], x) } +func (s *Sym) SetType(x uint8) { s[10] = x } +func (s *Sym) SetFlag(x uint8) { s[11] = x } +func (s *Sym) SetFlag2(x uint8) { s[12] = x } +func (s *Sym) SetSiz(x uint32) { binary.LittleEndian.PutUint32(s[13:], x) } +func (s *Sym) SetAlign(x uint32) { binary.LittleEndian.PutUint32(s[17:], x) } + +func (s *Sym) Write(w *Writer) { w.Bytes(s[:]) } + +// for testing +func (s *Sym) fromBytes(b []byte) { copy(s[:], b) } + +// Symbol reference. +type SymRef struct { + PkgIdx uint32 + SymIdx uint32 +} + +func (s SymRef) IsZero() bool { return s == SymRef{} } + +// Hash64 +type Hash64Type [Hash64Size]byte + +const Hash64Size = 8 + +// Hash +type HashType [HashSize]byte + +const HashSize = 16 // truncated SHA256 + +// Relocation. +// +// Serialized format: +// +// Reloc struct { +// Off int32 +// Siz uint8 +// Type uint16 +// Add int64 +// Sym SymRef +// } +type Reloc [RelocSize]byte + +const RelocSize = 4 + 1 + 2 + 8 + 8 + +func (r *Reloc) Off() int32 { return int32(binary.LittleEndian.Uint32(r[:])) } +func (r *Reloc) Siz() uint8 { return r[4] } +func (r *Reloc) Type() uint16 { return binary.LittleEndian.Uint16(r[5:]) } +func (r *Reloc) Add() int64 { return int64(binary.LittleEndian.Uint64(r[7:])) } +func (r *Reloc) Sym() SymRef { + return SymRef{binary.LittleEndian.Uint32(r[15:]), binary.LittleEndian.Uint32(r[19:])} +} + +func (r *Reloc) SetOff(x int32) { binary.LittleEndian.PutUint32(r[:], uint32(x)) } +func (r *Reloc) SetSiz(x uint8) { r[4] = x } +func (r *Reloc) SetType(x uint16) { binary.LittleEndian.PutUint16(r[5:], x) } +func (r *Reloc) SetAdd(x int64) { binary.LittleEndian.PutUint64(r[7:], uint64(x)) } +func (r *Reloc) SetSym(x SymRef) { + binary.LittleEndian.PutUint32(r[15:], x.PkgIdx) + binary.LittleEndian.PutUint32(r[19:], x.SymIdx) +} + +func (r *Reloc) Set(off int32, size uint8, typ uint16, add int64, sym SymRef) { + r.SetOff(off) + r.SetSiz(size) + r.SetType(typ) + r.SetAdd(add) + r.SetSym(sym) +} + +func (r *Reloc) Write(w *Writer) { w.Bytes(r[:]) } + +// for testing +func (r *Reloc) fromBytes(b []byte) { copy(r[:], b) } + +// Aux symbol info. +// +// Serialized format: +// +// Aux struct { +// Type uint8 +// Sym SymRef +// } +type Aux [AuxSize]byte + +const AuxSize = 1 + 8 + +// Aux Type +const ( + AuxGotype = iota + AuxFuncInfo + AuxFuncdata + AuxDwarfInfo + AuxDwarfLoc + AuxDwarfRanges + AuxDwarfLines + AuxPcsp + AuxPcfile + AuxPcline + AuxPcinline + AuxPcdata + AuxWasmImport + AuxWasmType + AuxSehUnwindInfo +) + +func (a *Aux) Type() uint8 { return a[0] } +func (a *Aux) Sym() SymRef { + return SymRef{binary.LittleEndian.Uint32(a[1:]), binary.LittleEndian.Uint32(a[5:])} +} + +func (a *Aux) SetType(x uint8) { a[0] = x } +func (a *Aux) SetSym(x SymRef) { + binary.LittleEndian.PutUint32(a[1:], x.PkgIdx) + binary.LittleEndian.PutUint32(a[5:], x.SymIdx) +} + +func (a *Aux) Write(w *Writer) { w.Bytes(a[:]) } + +// for testing +func (a *Aux) fromBytes(b []byte) { copy(a[:], b) } + +// Referenced symbol flags. +// +// Serialized format: +// +// RefFlags struct { +// Sym symRef +// Flag uint8 +// Flag2 uint8 +// } +type RefFlags [RefFlagsSize]byte + +const RefFlagsSize = 8 + 1 + 1 + +func (r *RefFlags) Sym() SymRef { + return SymRef{binary.LittleEndian.Uint32(r[:]), binary.LittleEndian.Uint32(r[4:])} +} +func (r *RefFlags) Flag() uint8 { return r[8] } +func (r *RefFlags) Flag2() uint8 { return r[9] } + +func (r *RefFlags) SetSym(x SymRef) { + binary.LittleEndian.PutUint32(r[:], x.PkgIdx) + binary.LittleEndian.PutUint32(r[4:], x.SymIdx) +} +func (r *RefFlags) SetFlag(x uint8) { r[8] = x } +func (r *RefFlags) SetFlag2(x uint8) { r[9] = x } + +func (r *RefFlags) Write(w *Writer) { w.Bytes(r[:]) } + +// Used to construct an artificially large array type when reading an +// item from the object file relocs section or aux sym section (needs +// to work on 32-bit as well as 64-bit). See issue 41621. +const huge = (1<<31 - 1) / RelocSize + +// Referenced symbol name. +// +// Serialized format: +// +// RefName struct { +// Sym symRef +// Name string +// } +type RefName [RefNameSize]byte + +const RefNameSize = 8 + stringRefSize + +func (n *RefName) Sym() SymRef { + return SymRef{binary.LittleEndian.Uint32(n[:]), binary.LittleEndian.Uint32(n[4:])} +} +func (n *RefName) Name(r *Reader) string { + len := binary.LittleEndian.Uint32(n[8:]) + off := binary.LittleEndian.Uint32(n[12:]) + return r.StringAt(off, len) +} + +func (n *RefName) SetSym(x SymRef) { + binary.LittleEndian.PutUint32(n[:], x.PkgIdx) + binary.LittleEndian.PutUint32(n[4:], x.SymIdx) +} +func (n *RefName) SetName(x string, w *Writer) { + binary.LittleEndian.PutUint32(n[8:], uint32(len(x))) + binary.LittleEndian.PutUint32(n[12:], w.stringOff(x)) +} + +func (n *RefName) Write(w *Writer) { w.Bytes(n[:]) } + +type Writer struct { + wr *bio.Writer + stringMap map[string]uint32 + off uint32 // running offset + + b [8]byte // scratch space for writing bytes +} + +func NewWriter(wr *bio.Writer) *Writer { + return &Writer{wr: wr, stringMap: make(map[string]uint32)} +} + +func (w *Writer) AddString(s string) { + if _, ok := w.stringMap[s]; ok { + return + } + w.stringMap[s] = w.off + w.RawString(s) +} + +func (w *Writer) stringOff(s string) uint32 { + off, ok := w.stringMap[s] + if !ok { + panic(fmt.Sprintf("writeStringRef: string not added: %q", s)) + } + return off +} + +func (w *Writer) StringRef(s string) { + w.Uint32(uint32(len(s))) + w.Uint32(w.stringOff(s)) +} + +func (w *Writer) RawString(s string) { + w.wr.WriteString(s) + w.off += uint32(len(s)) +} + +func (w *Writer) Bytes(s []byte) { + w.wr.Write(s) + w.off += uint32(len(s)) +} + +func (w *Writer) Uint64(x uint64) { + binary.LittleEndian.PutUint64(w.b[:], x) + w.wr.Write(w.b[:]) + w.off += 8 +} + +func (w *Writer) Uint32(x uint32) { + binary.LittleEndian.PutUint32(w.b[:4], x) + w.wr.Write(w.b[:4]) + w.off += 4 +} + +func (w *Writer) Uint16(x uint16) { + binary.LittleEndian.PutUint16(w.b[:2], x) + w.wr.Write(w.b[:2]) + w.off += 2 +} + +func (w *Writer) Uint8(x uint8) { + w.wr.WriteByte(x) + w.off++ +} + +func (w *Writer) Offset() uint32 { + return w.off +} + +type Reader struct { + b []byte // mmapped bytes, if not nil + readonly bool // whether b is backed with read-only memory + + start uint32 + h Header // keep block offsets +} + +func NewReaderFromBytes(b []byte, readonly bool) *Reader { + r := &Reader{b: b, readonly: readonly, start: 0} + err := r.h.Read(r) + if err != nil { + return nil + } + return r +} + +func (r *Reader) BytesAt(off uint32, len int) []byte { + if len == 0 { + return nil + } + end := int(off) + len + return r.b[int(off):end:end] +} + +func (r *Reader) uint64At(off uint32) uint64 { + b := r.BytesAt(off, 8) + return binary.LittleEndian.Uint64(b) +} + +func (r *Reader) uint32At(off uint32) uint32 { + b := r.BytesAt(off, 4) + return binary.LittleEndian.Uint32(b) +} + +func (r *Reader) StringAt(off uint32, len uint32) string { + b := r.b[off : off+len] + if r.readonly { + return toString(b) // backed by RO memory, ok to make unsafe string + } + return string(b) +} + +func toString(b []byte) string { + if len(b) == 0 { + return "" + } + return unsafe.String(&b[0], len(b)) +} + +func (r *Reader) StringRef(off uint32) string { + l := r.uint32At(off) + return r.StringAt(r.uint32At(off+4), l) +} + +func (r *Reader) Fingerprint() FingerprintType { + return r.h.Fingerprint +} + +func (r *Reader) Autolib() []ImportedPkg { + n := (r.h.Offsets[BlkAutolib+1] - r.h.Offsets[BlkAutolib]) / importedPkgSize + s := make([]ImportedPkg, n) + off := r.h.Offsets[BlkAutolib] + for i := range s { + s[i].Pkg = r.StringRef(off) + copy(s[i].Fingerprint[:], r.BytesAt(off+stringRefSize, len(s[i].Fingerprint))) + off += importedPkgSize + } + return s +} + +func (r *Reader) Pkglist() []string { + n := (r.h.Offsets[BlkPkgIdx+1] - r.h.Offsets[BlkPkgIdx]) / stringRefSize + s := make([]string, n) + off := r.h.Offsets[BlkPkgIdx] + for i := range s { + s[i] = r.StringRef(off) + off += stringRefSize + } + return s +} + +func (r *Reader) NPkg() int { + return int(r.h.Offsets[BlkPkgIdx+1]-r.h.Offsets[BlkPkgIdx]) / stringRefSize +} + +func (r *Reader) Pkg(i int) string { + off := r.h.Offsets[BlkPkgIdx] + uint32(i)*stringRefSize + return r.StringRef(off) +} + +func (r *Reader) NFile() int { + return int(r.h.Offsets[BlkFile+1]-r.h.Offsets[BlkFile]) / stringRefSize +} + +func (r *Reader) File(i int) string { + off := r.h.Offsets[BlkFile] + uint32(i)*stringRefSize + return r.StringRef(off) +} + +func (r *Reader) NSym() int { + return int(r.h.Offsets[BlkSymdef+1]-r.h.Offsets[BlkSymdef]) / SymSize +} + +func (r *Reader) NHashed64def() int { + return int(r.h.Offsets[BlkHashed64def+1]-r.h.Offsets[BlkHashed64def]) / SymSize +} + +func (r *Reader) NHasheddef() int { + return int(r.h.Offsets[BlkHasheddef+1]-r.h.Offsets[BlkHasheddef]) / SymSize +} + +func (r *Reader) NNonpkgdef() int { + return int(r.h.Offsets[BlkNonpkgdef+1]-r.h.Offsets[BlkNonpkgdef]) / SymSize +} + +func (r *Reader) NNonpkgref() int { + return int(r.h.Offsets[BlkNonpkgref+1]-r.h.Offsets[BlkNonpkgref]) / SymSize +} + +// SymOff returns the offset of the i-th symbol. +func (r *Reader) SymOff(i uint32) uint32 { + return r.h.Offsets[BlkSymdef] + i*SymSize +} + +// Sym returns a pointer to the i-th symbol. +func (r *Reader) Sym(i uint32) *Sym { + off := r.SymOff(i) + return (*Sym)(unsafe.Pointer(&r.b[off])) +} + +// NRefFlags returns the number of referenced symbol flags. +func (r *Reader) NRefFlags() int { + return int(r.h.Offsets[BlkRefFlags+1]-r.h.Offsets[BlkRefFlags]) / RefFlagsSize +} + +// RefFlags returns a pointer to the i-th referenced symbol flags. +// Note: here i is not a local symbol index, just a counter. +func (r *Reader) RefFlags(i int) *RefFlags { + off := r.h.Offsets[BlkRefFlags] + uint32(i*RefFlagsSize) + return (*RefFlags)(unsafe.Pointer(&r.b[off])) +} + +// Hash64 returns the i-th short hashed symbol's hash. +// Note: here i is the index of short hashed symbols, not all symbols +// (unlike other accessors). +func (r *Reader) Hash64(i uint32) uint64 { + off := r.h.Offsets[BlkHash64] + i*Hash64Size + return r.uint64At(off) +} + +// Hash returns a pointer to the i-th hashed symbol's hash. +// Note: here i is the index of hashed symbols, not all symbols +// (unlike other accessors). +func (r *Reader) Hash(i uint32) *HashType { + off := r.h.Offsets[BlkHash] + i*HashSize + return (*HashType)(unsafe.Pointer(&r.b[off])) +} + +// NReloc returns the number of relocations of the i-th symbol. +func (r *Reader) NReloc(i uint32) int { + relocIdxOff := r.h.Offsets[BlkRelocIdx] + i*4 + return int(r.uint32At(relocIdxOff+4) - r.uint32At(relocIdxOff)) +} + +// RelocOff returns the offset of the j-th relocation of the i-th symbol. +func (r *Reader) RelocOff(i uint32, j int) uint32 { + relocIdxOff := r.h.Offsets[BlkRelocIdx] + i*4 + relocIdx := r.uint32At(relocIdxOff) + return r.h.Offsets[BlkReloc] + (relocIdx+uint32(j))*uint32(RelocSize) +} + +// Reloc returns a pointer to the j-th relocation of the i-th symbol. +func (r *Reader) Reloc(i uint32, j int) *Reloc { + off := r.RelocOff(i, j) + return (*Reloc)(unsafe.Pointer(&r.b[off])) +} + +// Relocs returns a pointer to the relocations of the i-th symbol. +func (r *Reader) Relocs(i uint32) []Reloc { + off := r.RelocOff(i, 0) + n := r.NReloc(i) + return (*[huge]Reloc)(unsafe.Pointer(&r.b[off]))[:n:n] +} + +// NAux returns the number of aux symbols of the i-th symbol. +func (r *Reader) NAux(i uint32) int { + auxIdxOff := r.h.Offsets[BlkAuxIdx] + i*4 + return int(r.uint32At(auxIdxOff+4) - r.uint32At(auxIdxOff)) +} + +// AuxOff returns the offset of the j-th aux symbol of the i-th symbol. +func (r *Reader) AuxOff(i uint32, j int) uint32 { + auxIdxOff := r.h.Offsets[BlkAuxIdx] + i*4 + auxIdx := r.uint32At(auxIdxOff) + return r.h.Offsets[BlkAux] + (auxIdx+uint32(j))*uint32(AuxSize) +} + +// Aux returns a pointer to the j-th aux symbol of the i-th symbol. +func (r *Reader) Aux(i uint32, j int) *Aux { + off := r.AuxOff(i, j) + return (*Aux)(unsafe.Pointer(&r.b[off])) +} + +// Auxs returns the aux symbols of the i-th symbol. +func (r *Reader) Auxs(i uint32) []Aux { + off := r.AuxOff(i, 0) + n := r.NAux(i) + return (*[huge]Aux)(unsafe.Pointer(&r.b[off]))[:n:n] +} + +// DataOff returns the offset of the i-th symbol's data. +func (r *Reader) DataOff(i uint32) uint32 { + dataIdxOff := r.h.Offsets[BlkDataIdx] + i*4 + return r.h.Offsets[BlkData] + r.uint32At(dataIdxOff) +} + +// DataSize returns the size of the i-th symbol's data. +func (r *Reader) DataSize(i uint32) int { + dataIdxOff := r.h.Offsets[BlkDataIdx] + i*4 + return int(r.uint32At(dataIdxOff+4) - r.uint32At(dataIdxOff)) +} + +// Data returns the i-th symbol's data. +func (r *Reader) Data(i uint32) []byte { + dataIdxOff := r.h.Offsets[BlkDataIdx] + i*4 + base := r.h.Offsets[BlkData] + off := r.uint32At(dataIdxOff) + end := r.uint32At(dataIdxOff + 4) + return r.BytesAt(base+off, int(end-off)) +} + +// DataString returns the i-th symbol's data as a string. +func (r *Reader) DataString(i uint32) string { + dataIdxOff := r.h.Offsets[BlkDataIdx] + i*4 + base := r.h.Offsets[BlkData] + off := r.uint32At(dataIdxOff) + end := r.uint32At(dataIdxOff + 4) + return r.StringAt(base+off, end-off) +} + +// NRefName returns the number of referenced symbol names. +func (r *Reader) NRefName() int { + return int(r.h.Offsets[BlkRefName+1]-r.h.Offsets[BlkRefName]) / RefNameSize +} + +// RefName returns a pointer to the i-th referenced symbol name. +// Note: here i is not a local symbol index, just a counter. +func (r *Reader) RefName(i int) *RefName { + off := r.h.Offsets[BlkRefName] + uint32(i*RefNameSize) + return (*RefName)(unsafe.Pointer(&r.b[off])) +} + +// ReadOnly returns whether r.BytesAt returns read-only bytes. +func (r *Reader) ReadOnly() bool { + return r.readonly +} + +// Flags returns the flag bits read from the object file header. +func (r *Reader) Flags() uint32 { + return r.h.Flags +} + +func (r *Reader) Shared() bool { return r.Flags()&ObjFlagShared != 0 } +func (r *Reader) FromAssembly() bool { return r.Flags()&ObjFlagFromAssembly != 0 } +func (r *Reader) Unlinkable() bool { return r.Flags()&ObjFlagUnlinkable != 0 } +func (r *Reader) Std() bool { return r.Flags()&ObjFlagStd != 0 } diff --git a/internal/go/src/objabi/autotype.go b/internal/go/src/objabi/autotype.go new file mode 100755 index 0000000..f9d17a3 --- /dev/null +++ b/internal/go/src/objabi/autotype.go @@ -0,0 +1,38 @@ +// Derived from Inferno utils/6l/l.h and related files. +// https://bitbucket.org/inferno-os/inferno-os/src/master/utils/6l/l.h +// +// Copyright © 1994-1999 Lucent Technologies Inc. All rights reserved. +// Portions Copyright © 1995-1997 C H Forsyth (forsyth@terzarima.net) +// Portions Copyright © 1997-1999 Vita Nuova Limited +// Portions Copyright © 2000-2007 Vita Nuova Holdings Limited (www.vitanuova.com) +// Portions Copyright © 2004,2006 Bruce Ellis +// Portions Copyright © 2005-2007 C H Forsyth (forsyth@terzarima.net) +// Revisions Copyright © 2000-2007 Lucent Technologies Inc. and others +// Portions Copyright © 2009 The Go Authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package objabi + +// Auto.name +const ( + A_AUTO = 1 + iota + A_PARAM + A_DELETED_AUTO +) diff --git a/internal/go/src/objabi/flag.go b/internal/go/src/objabi/flag.go new file mode 100755 index 0000000..ad49061 --- /dev/null +++ b/internal/go/src/objabi/flag.go @@ -0,0 +1,374 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package objabi + +import ( + "flag" + "fmt" + "loov.dev/lensm/internal/go/src/bisect" + "loov.dev/lensm/internal/go/src/buildcfg" + "io" + "log" + "os" + "reflect" + "sort" + "strconv" + "strings" +) + +func Flagcount(name, usage string, val *int) { + flag.Var((*count)(val), name, usage) +} + +func Flagfn1(name, usage string, f func(string)) { + flag.Var(fn1(f), name, usage) +} + +func Flagprint(w io.Writer) { + flag.CommandLine.SetOutput(w) + flag.PrintDefaults() +} + +func Flagparse(usage func()) { + flag.Usage = usage + os.Args = expandArgs(os.Args) + flag.Parse() +} + +// expandArgs expands "response files" arguments in the provided slice. +// +// A "response file" argument starts with '@' and the rest of that +// argument is a filename with CR-or-CRLF-separated arguments. Each +// argument in the named files can also contain response file +// arguments. See Issue 18468. +// +// The returned slice 'out' aliases 'in' iff the input did not contain +// any response file arguments. +// +// TODO: handle relative paths of recursive expansions in different directories? +// Is there a spec for this? Are relative paths allowed? +func expandArgs(in []string) (out []string) { + // out is nil until we see a "@" argument. + for i, s := range in { + if strings.HasPrefix(s, "@") { + if out == nil { + out = make([]string, 0, len(in)*2) + out = append(out, in[:i]...) + } + slurp, err := os.ReadFile(s[1:]) + if err != nil { + log.Fatal(err) + } + args := strings.Split(strings.TrimSpace(strings.ReplaceAll(string(slurp), "\r", "")), "\n") + for i, arg := range args { + args[i] = DecodeArg(arg) + } + out = append(out, expandArgs(args)...) + } else if out != nil { + out = append(out, s) + } + } + if out == nil { + return in + } + return +} + +func AddVersionFlag() { + flag.Var(versionFlag{}, "V", "print version and exit") +} + +var buildID string // filled in by linker + +type versionFlag struct{} + +func (versionFlag) IsBoolFlag() bool { return true } +func (versionFlag) Get() any { return nil } +func (versionFlag) String() string { return "" } +func (versionFlag) Set(s string) error { + name := os.Args[0] + name = name[strings.LastIndex(name, `/`)+1:] + name = name[strings.LastIndex(name, `\`)+1:] + name = strings.TrimSuffix(name, ".exe") + + p := "" + + // If the enabled experiments differ from the baseline, + // include that difference. + if goexperiment := buildcfg.Experiment.String(); goexperiment != "" { + p = " X:" + goexperiment + } + + // The go command invokes -V=full to get a unique identifier + // for this tool. It is assumed that the release version is sufficient + // for releases, but during development we include the full + // build ID of the binary, so that if the compiler is changed and + // rebuilt, we notice and rebuild all packages. + if s == "full" { + if strings.Contains(buildcfg.Version, "devel") { + p += " buildID=" + buildID + } + } + + fmt.Printf("%s version %s%s\n", name, buildcfg.Version, p) + os.Exit(0) + return nil +} + +// count is a flag.Value that is like a flag.Bool and a flag.Int. +// If used as -name, it increments the count, but -name=x sets the count. +// Used for verbose flag -v. +type count int + +func (c *count) String() string { + return fmt.Sprint(int(*c)) +} + +func (c *count) Set(s string) error { + switch s { + case "true": + *c++ + case "false": + *c = 0 + default: + n, err := strconv.Atoi(s) + if err != nil { + return fmt.Errorf("invalid count %q", s) + } + *c = count(n) + } + return nil +} + +func (c *count) Get() any { + return int(*c) +} + +func (c *count) IsBoolFlag() bool { + return true +} + +func (c *count) IsCountFlag() bool { + return true +} + +type fn1 func(string) + +func (f fn1) Set(s string) error { + f(s) + return nil +} + +func (f fn1) String() string { return "" } + +// DecodeArg decodes an argument. +// +// This function is public for testing with the parallel encoder. +func DecodeArg(arg string) string { + // If no encoding, fastpath out. + if !strings.ContainsAny(arg, "\\\n") { + return arg + } + + var b strings.Builder + var wasBS bool + for _, r := range arg { + if wasBS { + switch r { + case '\\': + b.WriteByte('\\') + case 'n': + b.WriteByte('\n') + default: + // This shouldn't happen. The only backslashes that reach here + // should encode '\n' and '\\' exclusively. + panic("badly formatted input") + } + } else if r == '\\' { + wasBS = true + continue + } else { + b.WriteRune(r) + } + wasBS = false + } + return b.String() +} + +type debugField struct { + name string + help string + concurrentOk bool // true if this field/flag is compatible with concurrent compilation + val any // *int or *string +} + +type DebugFlag struct { + tab map[string]debugField + concurrentOk *bool // this is non-nil only for compiler's DebugFlags, but only compiler has concurrent:ok fields + debugSSA DebugSSA // this is non-nil only for compiler's DebugFlags. +} + +// A DebugSSA function is called to set a -d ssa/... option. +// If nil, those options are reported as invalid options. +// If DebugSSA returns a non-empty string, that text is reported as a compiler error. +// If phase is "help", it should print usage information and terminate the process. +type DebugSSA func(phase, flag string, val int, valString string) string + +// NewDebugFlag constructs a DebugFlag for the fields of debug, which +// must be a pointer to a struct. +// +// Each field of *debug is a different value, named for the lower-case of the field name. +// Each field must be an int or string and must have a `help` struct tag. +// There may be an "Any bool" field, which will be set if any debug flags are set. +// +// The returned flag takes a comma-separated list of settings. +// Each setting is name=value; for ints, name is short for name=1. +// +// If debugSSA is non-nil, any debug flags of the form ssa/... will be +// passed to debugSSA for processing. +func NewDebugFlag(debug any, debugSSA DebugSSA) *DebugFlag { + flag := &DebugFlag{ + tab: make(map[string]debugField), + debugSSA: debugSSA, + } + + v := reflect.ValueOf(debug).Elem() + t := v.Type() + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + ptr := v.Field(i).Addr().Interface() + if f.Name == "ConcurrentOk" { + switch ptr := ptr.(type) { + default: + panic("debug.ConcurrentOk must have type bool") + case *bool: + flag.concurrentOk = ptr + } + continue + } + name := strings.ToLower(f.Name) + help := f.Tag.Get("help") + if help == "" { + panic(fmt.Sprintf("debug.%s is missing help text", f.Name)) + } + concurrent := f.Tag.Get("concurrent") + + switch ptr.(type) { + default: + panic(fmt.Sprintf("debug.%s has invalid type %v (must be int, string, or *bisect.Matcher)", f.Name, f.Type)) + case *int, *string, **bisect.Matcher: + // ok + } + flag.tab[name] = debugField{name, help, concurrent == "ok", ptr} + } + + return flag +} + +func (f *DebugFlag) Set(debugstr string) error { + if debugstr == "" { + return nil + } + for name := range strings.SplitSeq(debugstr, ",") { + if name == "" { + continue + } + // display help about the debug option itself and quit + if name == "help" { + fmt.Print(debugHelpHeader) + maxLen, names := 0, []string{} + if f.debugSSA != nil { + maxLen = len("ssa/help") + } + for name := range f.tab { + if len(name) > maxLen { + maxLen = len(name) + } + names = append(names, name) + } + sort.Strings(names) + // Indent multi-line help messages. + nl := fmt.Sprintf("\n\t%-*s\t", maxLen, "") + for _, name := range names { + help := f.tab[name].help + fmt.Printf("\t%-*s\t%s\n", maxLen, name, strings.ReplaceAll(help, "\n", nl)) + } + if f.debugSSA != nil { + // ssa options have their own help + fmt.Printf("\t%-*s\t%s\n", maxLen, "ssa/help", "print help about SSA debugging") + } + os.Exit(0) + } + + val, valstring, haveInt := 1, "", true + if i := strings.IndexAny(name, "=:"); i >= 0 { + var err error + name, valstring = name[:i], name[i+1:] + val, err = strconv.Atoi(valstring) + if err != nil { + val, haveInt = 1, false + } + } + + if t, ok := f.tab[name]; ok { + switch vp := t.val.(type) { + case nil: + // Ignore + case *string: + *vp = valstring + case *int: + if !haveInt { + log.Fatalf("invalid debug value %v", name) + } + *vp = val + case **bisect.Matcher: + var err error + *vp, err = bisect.New(valstring) + if err != nil { + log.Fatalf("debug flag %v: %v", name, err) + } + default: + panic("bad debugtab type") + } + // assembler DebugFlags don't have a ConcurrentOk field to reset, so check against that. + if !t.concurrentOk && f.concurrentOk != nil { + *f.concurrentOk = false + } + } else if f.debugSSA != nil && strings.HasPrefix(name, "ssa/") { + // expect form ssa/phase/flag + // e.g. -d=ssa/generic_cse/time + // _ in phase name also matches space + phase := name[4:] + flag := "debug" // default flag is debug + if i := strings.Index(phase, "/"); i >= 0 { + flag = phase[i+1:] + phase = phase[:i] + } + err := f.debugSSA(phase, flag, val, valstring) + if err != "" { + log.Fatal(err) + } + // Setting this false for -d=ssa/... preserves old behavior + // of turning off concurrency for any debug flags. + // It's not known for sure if this is necessary, but it is safe. + *f.concurrentOk = false + + } else { + return fmt.Errorf("unknown debug key %s\n", name) + } + } + + return nil +} + +const debugHelpHeader = `usage: -d arg[,arg]* and arg is [=] + + is one of: + +` + +func (f *DebugFlag) String() string { + return "" +} diff --git a/internal/go/src/objabi/funcid.go b/internal/go/src/objabi/funcid.go new file mode 100755 index 0000000..8b8729c --- /dev/null +++ b/internal/go/src/objabi/funcid.go @@ -0,0 +1,52 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package objabi + +import ( + "loov.dev/lensm/internal/go/src/abi" + "strings" +) + +var funcIDs = map[string]abi.FuncID{ + "abort": abi.FuncID_abort, + "asmcgocall": abi.FuncID_asmcgocall, + "asyncPreempt": abi.FuncID_asyncPreempt, + "cgocallback": abi.FuncID_cgocallback, + "corostart": abi.FuncID_corostart, + "debugCallV2": abi.FuncID_debugCallV2, + "gcBgMarkWorker": abi.FuncID_gcBgMarkWorker, + "rt0_go": abi.FuncID_rt0_go, + "goexit": abi.FuncID_goexit, + "gogo": abi.FuncID_gogo, + "gopanic": abi.FuncID_gopanic, + "handleAsyncEvent": abi.FuncID_handleAsyncEvent, + "main": abi.FuncID_runtime_main, + "mcall": abi.FuncID_mcall, + "morestack": abi.FuncID_morestack, + "mstart": abi.FuncID_mstart, + "panicwrap": abi.FuncID_panicwrap, + "runFinalizers": abi.FuncID_runFinalizers, + "runCleanups": abi.FuncID_runCleanups, + "sigpanic": abi.FuncID_sigpanic, + "systemstack_switch": abi.FuncID_systemstack_switch, + "systemstack": abi.FuncID_systemstack, + + // Don't show in call stack but otherwise not special. + "deferreturn": abi.FuncIDWrapper, +} + +// Get the function ID for the named function in the named file. +// The function should be package-qualified. +func GetFuncID(name string, isWrapper bool) abi.FuncID { + if isWrapper { + return abi.FuncIDWrapper + } + if strings.HasPrefix(name, "runtime.") { + if id, ok := funcIDs[name[len("runtime."):]]; ok { + return id + } + } + return abi.FuncIDNormal +} diff --git a/internal/go/src/objabi/head.go b/internal/go/src/objabi/head.go new file mode 100755 index 0000000..3109b5c --- /dev/null +++ b/internal/go/src/objabi/head.go @@ -0,0 +1,114 @@ +// Derived from Inferno utils/6l/l.h and related files. +// https://bitbucket.org/inferno-os/inferno-os/src/master/utils/6l/l.h +// +// Copyright © 1994-1999 Lucent Technologies Inc. All rights reserved. +// Portions Copyright © 1995-1997 C H Forsyth (forsyth@terzarima.net) +// Portions Copyright © 1997-1999 Vita Nuova Limited +// Portions Copyright © 2000-2007 Vita Nuova Holdings Limited (www.vitanuova.com) +// Portions Copyright © 2004,2006 Bruce Ellis +// Portions Copyright © 2005-2007 C H Forsyth (forsyth@terzarima.net) +// Revisions Copyright © 2000-2007 Lucent Technologies Inc. and others +// Portions Copyright © 2009 The Go Authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package objabi + +import "fmt" + +// HeadType is the executable header type. +type HeadType uint8 + +const ( + Hunknown HeadType = iota + Hdarwin + Hdragonfly + Hfreebsd + Hjs + Hlinux + Hnetbsd + Hopenbsd + Hplan9 + Hsolaris + Hwasip1 + Hwindows + Haix +) + +func (h *HeadType) Set(s string) error { + switch s { + case "aix": + *h = Haix + case "darwin", "ios": + *h = Hdarwin + case "dragonfly": + *h = Hdragonfly + case "freebsd": + *h = Hfreebsd + case "js": + *h = Hjs + case "linux", "android": + *h = Hlinux + case "netbsd": + *h = Hnetbsd + case "openbsd": + *h = Hopenbsd + case "plan9": + *h = Hplan9 + case "illumos", "solaris": + *h = Hsolaris + case "wasip1": + *h = Hwasip1 + case "windows": + *h = Hwindows + default: + return fmt.Errorf("invalid headtype: %q", s) + } + return nil +} + +func (h HeadType) String() string { + switch h { + case Haix: + return "aix" + case Hdarwin: + return "darwin" + case Hdragonfly: + return "dragonfly" + case Hfreebsd: + return "freebsd" + case Hjs: + return "js" + case Hlinux: + return "linux" + case Hnetbsd: + return "netbsd" + case Hopenbsd: + return "openbsd" + case Hplan9: + return "plan9" + case Hsolaris: + return "solaris" + case Hwasip1: + return "wasip1" + case Hwindows: + return "windows" + } + return fmt.Sprintf("HeadType(%d)", h) +} diff --git a/internal/go/src/objabi/line.go b/internal/go/src/objabi/line.go new file mode 100755 index 0000000..e118ea1 --- /dev/null +++ b/internal/go/src/objabi/line.go @@ -0,0 +1,134 @@ +// Copyright 2009 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package objabi + +import ( + "loov.dev/lensm/internal/go/src/buildcfg" + "os" + "path/filepath" + "runtime" + "strings" +) + +// WorkingDir returns the current working directory +// (or "/???" if the directory cannot be identified), +// with "/" as separator. +func WorkingDir() string { + var path string + path, _ = os.Getwd() + if path == "" { + path = "/???" + } + return filepath.ToSlash(path) +} + +// AbsFile returns the absolute filename for file in the given directory, +// as rewritten by the rewrites argument. +// For unrewritten paths, AbsFile rewrites a leading $GOROOT prefix to the literal "$GOROOT". +// If the resulting path is the empty string, the result is "??". +// +// The rewrites argument is a ;-separated list of rewrites. +// Each rewrite is of the form "prefix" or "prefix=>replace", +// where prefix must match a leading sequence of path elements +// and is either removed entirely or replaced by the replacement. +func AbsFile(dir, file, rewrites string) string { + abs := file + if dir != "" && !filepath.IsAbs(file) { + abs = filepath.Join(dir, file) + } + + abs, rewritten := ApplyRewrites(abs, rewrites) + if !rewritten && buildcfg.GOROOT != "" && hasPathPrefix(abs, buildcfg.GOROOT) { + abs = "$GOROOT" + abs[len(buildcfg.GOROOT):] + } + + // Rewrite paths to match the slash convention of the target. + // This helps ensure that cross-compiled distributions remain + // bit-for-bit identical to natively compiled distributions. + if runtime.GOOS == "windows" { + abs = strings.ReplaceAll(abs, `\`, "/") + } + + if abs == "" { + abs = "??" + } + return abs +} + +// ApplyRewrites returns the filename for file in the given directory, +// as rewritten by the rewrites argument. +// +// The rewrites argument is a ;-separated list of rewrites. +// Each rewrite is of the form "prefix" or "prefix=>replace", +// where prefix must match a leading sequence of path elements +// and is either removed entirely or replaced by the replacement. +func ApplyRewrites(file, rewrites string) (string, bool) { + start := 0 + for i := 0; i <= len(rewrites); i++ { + if i == len(rewrites) || rewrites[i] == ';' { + if new, ok := applyRewrite(file, rewrites[start:i]); ok { + return new, true + } + start = i + 1 + } + } + + return file, false +} + +// applyRewrite applies the rewrite to the path, +// returning the rewritten path and a boolean +// indicating whether the rewrite applied at all. +func applyRewrite(path, rewrite string) (string, bool) { + prefix, replace := rewrite, "" + if j := strings.LastIndex(rewrite, "=>"); j >= 0 { + prefix, replace = rewrite[:j], rewrite[j+len("=>"):] + } + + if prefix == "" || !hasPathPrefix(path, prefix) { + return path, false + } + if len(path) == len(prefix) { + return replace, true + } + if replace == "" { + return path[len(prefix)+1:], true + } + return replace + path[len(prefix):], true +} + +// Does s have t as a path prefix? +// That is, does s == t or does s begin with t followed by a slash? +// For portability, we allow ASCII case folding, so that hasPathPrefix("a/b/c", "A/B") is true. +// Similarly, we allow slash folding, so that hasPathPrefix("a/b/c", "a\\b") is true. +// We do not allow full Unicode case folding, for fear of causing more confusion +// or harm than good. (For an example of the kinds of things that can go wrong, +// see http://article.gmane.org/gmane.linux.kernel/1853266.) +func hasPathPrefix(s string, t string) bool { + if len(t) > len(s) { + return false + } + var i int + for i = 0; i < len(t); i++ { + cs := int(s[i]) + ct := int(t[i]) + if 'A' <= cs && cs <= 'Z' { + cs += 'a' - 'A' + } + if 'A' <= ct && ct <= 'Z' { + ct += 'a' - 'A' + } + if cs == '\\' { + cs = '/' + } + if ct == '\\' { + ct = '/' + } + if cs != ct { + return false + } + } + return i >= len(s) || s[i] == '/' || s[i] == '\\' +} diff --git a/internal/go/src/objabi/path.go b/internal/go/src/objabi/path.go new file mode 100755 index 0000000..30301b1 --- /dev/null +++ b/internal/go/src/objabi/path.go @@ -0,0 +1,78 @@ +// Copyright 2017 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package objabi + +import ( + "fmt" + "strconv" + "strings" +) + +// PathToPrefix converts raw string to the prefix that will be used in the +// symbol table. All control characters, space, '%' and '"', as well as +// non-7-bit clean bytes turn into %xx. The period needs escaping only in the +// last segment of the path, and it makes for happier users if we escape that as +// little as possible. +func PathToPrefix(s string) string { + slash := strings.LastIndex(s, "/") + // check for chars that need escaping + n := 0 + for r := 0; r < len(s); r++ { + if c := s[r]; c <= ' ' || (c == '.' && r > slash) || c == '%' || c == '"' || c >= 0x7F { + n++ + } + } + + // quick exit + if n == 0 { + return s + } + + // escape + const hex = "0123456789abcdef" + p := make([]byte, 0, len(s)+2*n) + for r := 0; r < len(s); r++ { + if c := s[r]; c <= ' ' || (c == '.' && r > slash) || c == '%' || c == '"' || c >= 0x7F { + p = append(p, '%', hex[c>>4], hex[c&0xF]) + } else { + p = append(p, c) + } + } + + return string(p) +} + +// PrefixToPath is the inverse of PathToPrefix, replacing escape sequences with +// the original character. +func PrefixToPath(s string) (string, error) { + percent := strings.IndexByte(s, '%') + if percent == -1 { + return s, nil + } + + p := make([]byte, 0, len(s)) + for i := 0; i < len(s); { + if s[i] != '%' { + p = append(p, s[i]) + i++ + continue + } + if i+2 >= len(s) { + // Not enough characters remaining to be a valid escape + // sequence. + return "", fmt.Errorf("malformed prefix %q: escape sequence must contain two hex digits", s) + } + + b, err := strconv.ParseUint(s[i+1:i+3], 16, 8) + if err != nil { + // Not a valid escape sequence. + return "", fmt.Errorf("malformed prefix %q: escape sequence %q must contain two hex digits", s, s[i:i+3]) + } + + p = append(p, byte(b)) + i += 3 + } + return string(p), nil +} diff --git a/internal/go/src/objabi/pkgspecial.go b/internal/go/src/objabi/pkgspecial.go new file mode 100755 index 0000000..7e49962 --- /dev/null +++ b/internal/go/src/objabi/pkgspecial.go @@ -0,0 +1,136 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package objabi + +import "sync" + +// PkgSpecial indicates special build properties of a given runtime-related +// package. +type PkgSpecial struct { + // Runtime indicates that this package is "runtime" or imported by + // "runtime". This has several effects (which maybe should be split out): + // + // - Implicit allocation is disallowed. + // + // - Various runtime pragmas are enabled. + // + // - Optimizations are always enabled. + // + // - Checkptr is always disabled. + // + // This should be set for runtime and all packages it imports, and may be + // set for additional packages. + Runtime bool + + // NoInstrument indicates this package should not receive sanitizer + // instrumentation. In many of these, instrumentation could cause infinite + // recursion. This is all runtime packages, plus those that support the + // sanitizers. + NoInstrument bool + + // NoRaceFunc indicates functions in this package should not get + // racefuncenter/racefuncexit instrumentation Memory accesses in these + // packages are either uninteresting or will cause false positives. + NoRaceFunc bool + + // AllowAsmABI indicates that assembly in this package is allowed to use ABI + // selectors in symbol names. Generally this is needed for packages that + // interact closely with the runtime package or have performance-critical + // assembly. + AllowAsmABI bool +} + +var runtimePkgs = []string{ + // TODO(panjf2000): consider syncing the list inside the + // isAsyncSafePoint in preempt.go based on this list? + + "runtime", + + "loov.dev/lensm/internal/go/src/runtime/atomic", + "loov.dev/lensm/internal/go/src/runtime/cgroup", + "loov.dev/lensm/internal/go/src/runtime/exithook", + "loov.dev/lensm/internal/go/src/runtime/gc", + "loov.dev/lensm/internal/go/src/runtime/gc/scan", + "loov.dev/lensm/internal/go/src/runtime/maps", + "loov.dev/lensm/internal/go/src/runtime/math", + "loov.dev/lensm/internal/go/src/runtime/sys", + "loov.dev/lensm/internal/go/src/runtime/syscall/linux", + "loov.dev/lensm/internal/go/src/runtime/syscall/windows", + + "loov.dev/lensm/internal/go/src/abi", + "loov.dev/lensm/internal/go/src/bytealg", + "loov.dev/lensm/internal/go/src/byteorder", + "loov.dev/lensm/internal/go/src/chacha8rand", + "loov.dev/lensm/internal/go/src/coverage/rtcov", + "loov.dev/lensm/internal/go/src/cpu", + "loov.dev/lensm/internal/go/src/goarch", + "loov.dev/lensm/internal/go/src/godebugs", + "loov.dev/lensm/internal/go/src/goexperiment", + "loov.dev/lensm/internal/go/src/goos", + "loov.dev/lensm/internal/go/src/profilerecord", + "loov.dev/lensm/internal/go/src/strconv", + "loov.dev/lensm/internal/go/src/stringslite", +} + +// extraNoInstrumentPkgs is the set of packages in addition to runtimePkgs that +// should have NoInstrument set. +var extraNoInstrumentPkgs = []string{ + "runtime/race", + "runtime/msan", + "runtime/asan", + // We omit bytealg even though it's imported by runtime because it also + // backs a lot of package bytes. Currently we don't have a way to omit race + // instrumentation when used from the runtime while keeping race + // instrumentation when used from user code. Somehow this doesn't seem to + // cause problems, though we may be skating on thin ice. See #61204. + "-internal/bytealg", +} + +var noRaceFuncPkgs = []string{"sync", "sync/atomic", "internal/sync", "internal/runtime/atomic"} + +var allowAsmABIPkgs = []string{ + "runtime", + "reflect", + "syscall", + "loov.dev/lensm/internal/go/src/bytealg", + "loov.dev/lensm/internal/go/src/chacha8rand", + "loov.dev/lensm/internal/go/src/runtime/syscall/linux", + "loov.dev/lensm/internal/go/src/runtime/syscall/windows", + "loov.dev/lensm/internal/go/src/runtime/startlinetest", +} + +// LookupPkgSpecial returns special build properties for the given package path. +func LookupPkgSpecial(pkgPath string) PkgSpecial { + return pkgSpecialsOnce()[pkgPath] +} + +var pkgSpecialsOnce = sync.OnceValue(func() map[string]PkgSpecial { + // Construct pkgSpecials from various package lists. This lets us use + // more flexible logic, while keeping the final map simple, and avoids + // the init-time cost of a map. + pkgSpecials := make(map[string]PkgSpecial) + set := func(elt string, f func(*PkgSpecial)) { + s := pkgSpecials[elt] + f(&s) + pkgSpecials[elt] = s + } + for _, pkg := range runtimePkgs { + set(pkg, func(ps *PkgSpecial) { ps.Runtime = true; ps.NoInstrument = true }) + } + for _, pkg := range extraNoInstrumentPkgs { + if pkg[0] == '-' { + set(pkg[1:], func(ps *PkgSpecial) { ps.NoInstrument = false }) + } else { + set(pkg, func(ps *PkgSpecial) { ps.NoInstrument = true }) + } + } + for _, pkg := range noRaceFuncPkgs { + set(pkg, func(ps *PkgSpecial) { ps.NoRaceFunc = true }) + } + for _, pkg := range allowAsmABIPkgs { + set(pkg, func(ps *PkgSpecial) { ps.AllowAsmABI = true }) + } + return pkgSpecials +}) diff --git a/internal/go/src/objabi/reloctype.go b/internal/go/src/objabi/reloctype.go new file mode 100755 index 0000000..aef8900 --- /dev/null +++ b/internal/go/src/objabi/reloctype.go @@ -0,0 +1,544 @@ +// Derived from Inferno utils/6l/l.h and related files. +// https://bitbucket.org/inferno-os/inferno-os/src/master/utils/6l/l.h +// +// Copyright © 1994-1999 Lucent Technologies Inc. All rights reserved. +// Portions Copyright © 1995-1997 C H Forsyth (forsyth@terzarima.net) +// Portions Copyright © 1997-1999 Vita Nuova Limited +// Portions Copyright © 2000-2007 Vita Nuova Holdings Limited (www.vitanuova.com) +// Portions Copyright © 2004,2006 Bruce Ellis +// Portions Copyright © 2005-2007 C H Forsyth (forsyth@terzarima.net) +// Revisions Copyright © 2000-2007 Lucent Technologies Inc. and others +// Portions Copyright © 2009 The Go Authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package objabi + +type RelocType int16 + +//go:generate stringer -type=RelocType +const ( + R_ADDR RelocType = 1 + iota + // R_ADDRPOWER relocates a pair of "D-form" instructions (instructions with 16-bit + // immediates in the low half of the instruction word), usually addis followed by + // another add or a load, inserting the "high adjusted" 16 bits of the address of + // the referenced symbol into the immediate field of the first instruction and the + // low 16 bits into that of the second instruction. + R_ADDRPOWER + // R_ADDRARM64 relocates an adrp, add pair to compute the address of the + // referenced symbol. + R_ADDRARM64 + // R_ADDRMIPS (only used on mips/mips64) resolves to the low 16 bits of an external + // address, by encoding it into the instruction. + R_ADDRMIPS + // R_ADDROFF resolves to a 32-bit offset from the beginning of the section + // holding the data being relocated to the referenced symbol. + R_ADDROFF + R_SIZE + R_CALL + R_CALLARM + R_CALLARM64 + R_CALLIND + R_CALLPOWER + // R_CALLMIPS (only used on mips64) resolves to non-PC-relative target address + // of a CALL (JAL) instruction, by encoding the address into the instruction. + R_CALLMIPS + R_CONST + R_PCREL + // R_TLS_LE, used on 386, amd64, and ARM, resolves to the offset of the + // thread-local symbol from the thread local base and is used to implement the + // "local exec" model for tls access (r.Sym is not set on intel platforms but is + // set to a TLS symbol -- runtime.tlsg -- in the linker when externally linking). + R_TLS_LE + // R_TLS_IE, used 386, amd64, and ARM resolves to the PC-relative offset to a GOT + // slot containing the offset from the thread-local symbol from the thread local + // base and is used to implemented the "initial exec" model for tls access (r.Sym + // is not set on intel platforms but is set to a TLS symbol -- runtime.tlsg -- in + // the linker when externally linking). + R_TLS_IE + R_GOTOFF + R_PLT0 + R_PLT1 + R_PLT2 + R_USEFIELD + // R_USETYPE resolves to an *rtype, but no relocation is created. The + // linker uses this as a signal that the pointed-to type information + // should be linked into the final binary, even if there are no other + // direct references. (This is used for types reachable by reflection.) + R_USETYPE + // R_USEIFACE marks a type is converted to an interface in the function this + // relocation is applied to. The target is a type descriptor or an itab + // (in the latter case it refers to the concrete type contained in the itab). + // This is a marker relocation (0-sized), for the linker's reachabililty + // analysis. + R_USEIFACE + // R_USEIFACEMETHOD marks an interface method that is used in the function + // this relocation is applied to. The target is an interface type descriptor. + // The addend is the offset of the method in the type descriptor. + // This is a marker relocation (0-sized), for the linker's reachabililty + // analysis. + R_USEIFACEMETHOD + // R_USENAMEDMETHOD marks that methods with a specific name must not be eliminated. + // The target is a symbol containing the name of a method called via a generic + // interface or looked up via MethodByName("F"). + R_USENAMEDMETHOD + // R_METHODOFF resolves to a 32-bit offset from the beginning of the section + // holding the data being relocated to the referenced symbol. + // It is a variant of R_ADDROFF used when linking from the uncommonType of a + // *rtype, and may be set to zero by the linker if it determines the method + // text is unreachable by the linked program. + R_METHODOFF + // R_KEEP tells the linker to keep the referred-to symbol in the final binary + // if the symbol containing the R_KEEP relocation is in the final binary. + R_KEEP + R_POWER_TOC + R_GOTPCREL + // R_JMPMIPS (only used on mips64) resolves to non-PC-relative target address + // of a JMP instruction, by encoding the address into the instruction. + // The stack nosplit check ignores this since it is not a function call. + R_JMPMIPS + + // R_DWARFSECREF resolves to the offset of the symbol from its section. + // Target of relocation must be size 4 (in current implementation). + R_DWARFSECREF + + // Platform dependent relocations. Architectures with fixed width instructions + // have the inherent issue that a 32-bit (or 64-bit!) displacement cannot be + // stuffed into a 32-bit instruction, so an address needs to be spread across + // several instructions, and in turn this requires a sequence of relocations, each + // updating a part of an instruction. This leads to relocation codes that are + // inherently processor specific. + + // Arm64. + + // Set a MOV[NZ] immediate field to bits [15:0] of the offset from the thread + // local base to the thread local variable defined by the referenced (thread + // local) symbol. Error if the offset does not fit into 16 bits. + R_ARM64_TLS_LE + + // Relocates an ADRP; LD64 instruction sequence to load the offset between + // the thread local base and the thread local variable defined by the + // referenced (thread local) symbol from the GOT. + R_ARM64_TLS_IE + + // R_ARM64_GOTPCREL relocates an adrp, ld64 pair to compute the address of the GOT + // slot of the referenced symbol. + R_ARM64_GOTPCREL + + // R_ARM64_GOT resolves a GOT-relative instruction sequence, usually an adrp + // followed by another ld instruction. + R_ARM64_GOT + + // R_ARM64_PCREL resolves a PC-relative addresses instruction sequence, usually an + // adrp followed by another add instruction. + R_ARM64_PCREL + + // R_ARM64_PCREL_LDST8 resolves a PC-relative addresses instruction sequence, usually an + // adrp followed by a LD8 or ST8 instruction. + R_ARM64_PCREL_LDST8 + + // R_ARM64_PCREL_LDST16 resolves a PC-relative addresses instruction sequence, usually an + // adrp followed by a LD16 or ST16 instruction. + R_ARM64_PCREL_LDST16 + + // R_ARM64_PCREL_LDST32 resolves a PC-relative addresses instruction sequence, usually an + // adrp followed by a LD32 or ST32 instruction. + R_ARM64_PCREL_LDST32 + + // R_ARM64_PCREL_LDST64 resolves a PC-relative addresses instruction sequence, usually an + // adrp followed by a LD64 or ST64 instruction. + R_ARM64_PCREL_LDST64 + + // R_ARM64_LDST8 sets a LD/ST immediate value to bits [11:0] of a local address. + R_ARM64_LDST8 + + // R_ARM64_LDST16 sets a LD/ST immediate value to bits [11:1] of a local address. + R_ARM64_LDST16 + + // R_ARM64_LDST32 sets a LD/ST immediate value to bits [11:2] of a local address. + R_ARM64_LDST32 + + // R_ARM64_LDST64 sets a LD/ST immediate value to bits [11:3] of a local address. + R_ARM64_LDST64 + + // R_ARM64_LDST128 sets a LD/ST immediate value to bits [11:4] of a local address. + R_ARM64_LDST128 + + // PPC64. + + // R_POWER_TLS_LE is used to implement the "local exec" model for tls + // access. It resolves to the offset of the thread-local symbol from the + // thread pointer (R13) and is split against a pair of instructions to + // support a 32 bit displacement. + R_POWER_TLS_LE + + // R_POWER_TLS_IE is used to implement the "initial exec" model for tls access. It + // relocates a D-form, DS-form instruction sequence like R_ADDRPOWER_DS. It + // inserts to the offset of GOT slot for the thread-local symbol from the TOC (the + // GOT slot is filled by the dynamic linker with the offset of the thread-local + // symbol from the thread pointer (R13)). + R_POWER_TLS_IE + + // R_POWER_TLS marks an X-form instruction such as "ADD R3,R13,R4" as completing + // a sequence of GOT-relative relocations to compute a TLS address. This can be + // used by the system linker to rewrite the GOT-relative TLS relocation into a + // simpler thread-pointer relative relocation. See table 3.26 and 3.28 in the + // ppc64 elfv2 1.4 ABI on this transformation. Likewise, the second argument + // (usually called RB in X-form instructions) is assumed to be R13. + R_POWER_TLS + + // R_POWER_TLS_IE_PCREL34 is similar to R_POWER_TLS_IE, but marks a single MOVD + // which has been assembled as a single prefixed load doubleword without using the + // TOC. + R_POWER_TLS_IE_PCREL34 + + // R_POWER_TLS_LE_TPREL34 is similar to R_POWER_TLS_LE, but computes an offset from + // the thread pointer in one prefixed instruction. + R_POWER_TLS_LE_TPREL34 + + // R_ADDRPOWER_DS is similar to R_ADDRPOWER above, but assumes the second + // instruction is a "DS-form" instruction, which has an immediate field occupying + // bits [15:2] of the instruction word. Bits [15:2] of the address of the + // relocated symbol are inserted into this field; it is an error if the last two + // bits of the address are not 0. + R_ADDRPOWER_DS + + // R_ADDRPOWER_GOT relocates a D-form + DS-form instruction sequence by inserting + // a relative displacement of referenced symbol's GOT entry to the TOC pointer. + R_ADDRPOWER_GOT + + // R_ADDRPOWER_GOT_PCREL34 is identical to R_ADDRPOWER_GOT, but uses a PC relative + // sequence to generate a GOT symbol addresses. + R_ADDRPOWER_GOT_PCREL34 + + // R_ADDRPOWER_PCREL relocates two D-form instructions like R_ADDRPOWER, but + // inserts the displacement from the place being relocated to the address of the + // relocated symbol instead of just its address. + R_ADDRPOWER_PCREL + + // R_ADDRPOWER_TOCREL relocates two D-form instructions like R_ADDRPOWER, but + // inserts the offset from the TOC to the address of the relocated symbol + // rather than the symbol's address. + R_ADDRPOWER_TOCREL + + // R_ADDRPOWER_TOCREL_DS relocates a D-form, DS-form instruction sequence like + // R_ADDRPOWER_DS but inserts the offset from the TOC to the address of the + // relocated symbol rather than the symbol's address. + R_ADDRPOWER_TOCREL_DS + + // R_ADDRPOWER_D34 relocates a single prefixed D-form load/store operation. All + // prefixed forms are D form. The high 18 bits are stored in the prefix, + // and the low 16 are stored in the suffix. The address is absolute. + R_ADDRPOWER_D34 + + // R_ADDRPOWER_PCREL34 relates a single prefixed D-form load/store/add operation. + // All prefixed forms are D form. The resulting address is relative to the + // PC. It is a signed 34 bit offset. + R_ADDRPOWER_PCREL34 + + // RISC-V. + + // R_RISCV_JAL resolves a 20 bit offset for a J-type instruction. + R_RISCV_JAL + + // R_RISCV_JAL_TRAMP is the same as R_RISCV_JAL but denotes the use of a + // trampoline, which we may be able to avoid during relocation. These are + // only used by the linker and are not emitted by the compiler or assembler. + R_RISCV_JAL_TRAMP + + // R_RISCV_CALL resolves a 32 bit PC-relative address for an AUIPC + JALR + // instruction pair. + R_RISCV_CALL + + // R_RISCV_PCREL_ITYPE resolves a 32 bit PC-relative address for an + // AUIPC + I-type instruction pair. + R_RISCV_PCREL_ITYPE + + // R_RISCV_PCREL_STYPE resolves a 32 bit PC-relative address for an + // AUIPC + S-type instruction pair. + R_RISCV_PCREL_STYPE + + // R_RISCV_TLS_IE resolves a 32 bit TLS initial-exec address for an + // AUIPC + I-type instruction pair. + R_RISCV_TLS_IE + + // R_RISCV_TLS_LE resolves a 32 bit TLS local-exec address for a + // LUI + I-type instruction sequence. + R_RISCV_TLS_LE + + // R_RISCV_GOT_HI20 resolves the high 20 bits of a 32-bit PC-relative GOT + // address. + R_RISCV_GOT_HI20 + + // R_RISCV_GOT_PCREL_ITYPE resolves a 32-bit PC-relative GOT entry + // address for an AUIPC + I-type instruction pair. + R_RISCV_GOT_PCREL_ITYPE + + // R_RISCV_PCREL_HI20 resolves the high 20 bits of a 32-bit PC-relative + // address. + R_RISCV_PCREL_HI20 + + // R_RISCV_PCREL_LO12_I resolves the low 12 bits of a 32-bit PC-relative + // address using an I-type instruction. + R_RISCV_PCREL_LO12_I + + // R_RISCV_PCREL_LO12_S resolves the low 12 bits of a 32-bit PC-relative + // address using an S-type instruction. + R_RISCV_PCREL_LO12_S + + // R_RISCV_BRANCH resolves a 12-bit PC-relative branch offset. + R_RISCV_BRANCH + + // R_RISCV_ADD32 resolves a 32-bit label addition, being the stored value, + // plus the symbol address plus the addend (V + S + A). + R_RISCV_ADD32 + + // R_RISCV_SUB32 resolves a 32-bit label subtraction, being the stored value, + // minus the symbol address minus the addend (V - S - A). + R_RISCV_SUB32 + + // R_RISCV_RVC_BRANCH resolves an 8-bit PC-relative offset for a CB-type + // instruction. + R_RISCV_RVC_BRANCH + + // R_RISCV_RVC_JUMP resolves an 11-bit PC-relative offset for a CJ-type + // instruction. + R_RISCV_RVC_JUMP + + // R_PCRELDBL relocates s390x 2-byte aligned PC-relative addresses. + // TODO(mundaym): remove once variants can be serialized - see issue 14218. + R_PCRELDBL + + // Loong64. + + // R_LOONG64_ADDR_HI resolves to the sign-adjusted "upper" 20 bits (bit 5-24) of an + // external address, by encoding it into the instruction. + // R_LOONG64_ADDR_LO resolves to the low 12 bits of an external address, by encoding + // it into the instruction. + R_LOONG64_ADDR_HI + R_LOONG64_ADDR_LO + + // R_LOONG64_ADDR_PCREL20_S2 resolves to the 22-bit, 4-byte aligned offset of an + // external address, by encoding it into a PCADDI instruction. + R_LOONG64_ADDR_PCREL20_S2 + + // R_LOONG64_TLS_LE_HI resolves to the high 20 bits of a TLS address (offset from + // thread pointer), by encoding it into the instruction. + // R_LOONG64_TLS_LE_LO resolves to the low 12 bits of a TLS address (offset from + // thread pointer), by encoding it into the instruction. + R_LOONG64_TLS_LE_HI + R_LOONG64_TLS_LE_LO + + // R_CALLLOONG64 resolves to the 28-bit 4-byte aligned PC-relative target + // address of a BL instruction, by encoding it into the instruction. + R_CALLLOONG64 + + // R_LOONG64_CALL36 resolves to the 38-bit 4-byte aligned PC-relative target + // address of a PCADDU18I + JIRL pair, by encoding it into the instructions. + R_LOONG64_CALL36 + + // R_LOONG64_TLS_IE_HI and R_LOONG64_TLS_IE_LO relocates a pcalau12i, ld.d + // pair to compute the address of the GOT slot of the tls symbol. + R_LOONG64_TLS_IE_HI + R_LOONG64_TLS_IE_LO + + // R_LOONG64_GOT_HI and R_LOONG64_GOT_LO resolves a GOT-relative instruction sequence, + // usually an pcalau12i followed by another ld or addi instruction. + R_LOONG64_GOT_HI + R_LOONG64_GOT_LO + + // 64-bit in-place addition. + R_LOONG64_ADD64 + // 64-bit in-place subtraction. + R_LOONG64_SUB64 + + // R_JMP16LOONG64 resolves to the 18-bit 4-byte aligned PC-relative target + // address of a BEQ/BNE/BLT/BGE/BLTU/BGEU instruction, by encoding it into + // the instruction. + R_JMP16LOONG64 + + // R_JMP21LOONG64 resolves to the 23-bit 4-byte aligned PC-relative target + // address of a BEQZ/BNEZ instruction, by encoding it into the instruction. + R_JMP21LOONG64 + + // R_JMPLOONG64 resolves to the 28-bit 4-byte aligned PC-relative target + // address of a B instruction, by encoding it into the instruction. + R_JMPLOONG64 + + // R_ADDRMIPSU (only used on mips/mips64) resolves to the sign-adjusted "upper" 16 + // bits (bit 16-31) of an external address, by encoding it into the instruction. + R_ADDRMIPSU + // R_ADDRMIPSTLS (only used on mips64) resolves to the low 16 bits of a TLS + // address (offset from thread pointer), by encoding it into the instruction. + R_ADDRMIPSTLS + + // R_ADDRCUOFF resolves to a pointer-sized offset from the start of the + // symbol's DWARF compile unit. + R_ADDRCUOFF + + // R_WASMIMPORT resolves to the index of the WebAssembly function import. + R_WASMIMPORT + + // R_XCOFFREF (only used on aix/ppc64) prevents garbage collection by ld + // of a symbol. This isn't a real relocation, it can be placed in anywhere + // in a symbol and target any symbols. + R_XCOFFREF + + // R_PEIMAGEOFF resolves to a 32-bit offset from the start address of where + // the executable file is mapped in memory. + R_PEIMAGEOFF + + // R_INITORDER specifies an ordering edge between two inittask records. + // (From one p..inittask record to another one.) + // This relocation does not apply any changes to the actual data, it is + // just used in the linker to order the inittask records appropriately. + R_INITORDER + + // The R_DWTXTADDR_* family of relocations are effectively + // references to the .debug_addr entry for a given TEXT symbol + // corresponding to a Go function. Given a R_DWTXTADDR_* reloc + // applied to dwarf section S at offset O against sym F, the linker + // locates the .debug_addr entry for F (within its package) and + // writes the index of that entry to section S at offset O, using + // ULEB encoding, writing a number of bytes controlled by the + // suffix (e.g. for R_DWTXTADDR_U2 we write two bytes). Note + // also that .debug_addr indices are not finalized until link time; + // when the compiler creates a R_DWTXTADDR_* relocation the + // index payload will be left as zero (to be filled in later). + R_DWTXTADDR_U1 + R_DWTXTADDR_U2 + R_DWTXTADDR_U3 + R_DWTXTADDR_U4 + + // R_WEAK marks the relocation as a weak reference. + // A weak relocation does not make the symbol it refers to reachable, + // and is only honored by the linker if the symbol is in some other way + // reachable. + R_WEAK = -1 << 15 + + R_WEAKADDR = R_WEAK | R_ADDR + R_WEAKADDROFF = R_WEAK | R_ADDROFF +) + +// IsDirectCall reports whether r is a relocation for a direct call. +// A direct call is a CALL instruction that takes the target address +// as an immediate. The address is embedded into the instruction(s), possibly +// with limited width. An indirect call is a CALL instruction that takes +// the target address in register or memory. +func (r RelocType) IsDirectCall() bool { + switch r { + case R_CALL, R_CALLARM, R_CALLARM64, R_CALLLOONG64, R_CALLMIPS, R_CALLPOWER, + R_RISCV_CALL, R_RISCV_JAL, R_RISCV_JAL_TRAMP: + return true + } + return false +} + +// IsDirectJump reports whether r is a relocation for a direct jump. +// A direct jump is a JMP instruction that takes the target address +// as an immediate. The address is embedded into the instruction, possibly +// with limited width. An indirect jump is a JMP instruction that takes +// the target address in register or memory. +func (r RelocType) IsDirectJump() bool { + switch r { + case R_JMPMIPS: + return true + case R_JMPLOONG64: + return true + } + return false +} + +// IsDirectCallOrJump reports whether r is a relocation for a direct +// call or a direct jump. +func (r RelocType) IsDirectCallOrJump() bool { + return r.IsDirectCall() || r.IsDirectJump() +} + +// IsDwTxtAddr reports whether r is one of the several DWARF +// .debug_addr section indirect relocations. +func (r RelocType) IsDwTxtAddr() bool { + switch r { + case R_DWTXTADDR_U1, R_DWTXTADDR_U2, R_DWTXTADDR_U3, R_DWTXTADDR_U4: + return true + default: + return false + } +} + +// FuncCountToDwTxtAddrFlavor returns the correct DWARF .debug_addr +// section relocation to use when compiling a package with a total of +// fncount functions, along with the size of the ULEB128-encoded blob +// needed to store the eventual .debug_addr index. +func FuncCountToDwTxtAddrFlavor(fncount int) (RelocType, int) { + switch { + case fncount <= 127: + return R_DWTXTADDR_U1, 1 + case fncount <= 16383: + return R_DWTXTADDR_U2, 2 + case fncount <= 2097151: + return R_DWTXTADDR_U3, 3 + case fncount <= 268435455: + return R_DWTXTADDR_U4, 4 + default: + panic("package has more than 268435455 functions") + } +} + +// DummyDwarfFunctionCountForAssembler returns a dummy value to be +// used for "total number of functions in the package" for use in the +// assembler (compiler does not call this function). +// +// Background/motivation: let's say we have a package P with some +// assembly functions (in "a.s") and some Go functions (in +// "b.go"). The compilation sequence used by the Go commmand will be: +// +// 1. run the assembler on a.s to generate a "symabis" file +// 2. run the compiler on b.go passing it the symabis file and generating a "go_defs.h" asm header +// 3. run the assembler on a.s passing it an include dir with the generated "go_defs.h" file +// +// When the compiler runs, it can easily determine the total function +// count for the package (for use with FuncCountToDwTxtAddrFlavor +// above) by counting defined Go funcs and looking at the symabis +// file. With the assembler however there is no easy way for it to +// figure out the total number of Go source funcs. To keep things +// simple, we instead just use a dummy total function count while +// running the assembler that will guarantee we pick a relocation +// flavor that will work for any package size. +func DummyDwarfFunctionCountForAssembler() int { + return 9999999 +} + +// DwTxtAddrRelocParams returns the maximum number of functions per +// package supported for the DWARF .debug_addr relocation variant r, +// along with the number of bytes it takes up in encoded form. +func (r RelocType) DwTxtAddrRelocParams() (int, int) { + switch r { + case R_DWTXTADDR_U1: + return 0x7f, 1 + case R_DWTXTADDR_U2: + return 0x3fff, 2 + case R_DWTXTADDR_U3: + return 0x1fffff, 3 + case R_DWTXTADDR_U4: + return 0xfffffff, 4 + default: + panic("not a dwtxtaddr relocation") + } +} diff --git a/internal/go/src/objabi/reloctype_string.go b/internal/go/src/objabi/reloctype_string.go new file mode 100755 index 0000000..9137bae --- /dev/null +++ b/internal/go/src/objabi/reloctype_string.go @@ -0,0 +1,126 @@ +// Code generated by "stringer -type=RelocType"; DO NOT EDIT. + +package objabi + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[R_ADDR-1] + _ = x[R_ADDRPOWER-2] + _ = x[R_ADDRARM64-3] + _ = x[R_ADDRMIPS-4] + _ = x[R_ADDROFF-5] + _ = x[R_SIZE-6] + _ = x[R_CALL-7] + _ = x[R_CALLARM-8] + _ = x[R_CALLARM64-9] + _ = x[R_CALLIND-10] + _ = x[R_CALLPOWER-11] + _ = x[R_CALLMIPS-12] + _ = x[R_CONST-13] + _ = x[R_PCREL-14] + _ = x[R_TLS_LE-15] + _ = x[R_TLS_IE-16] + _ = x[R_GOTOFF-17] + _ = x[R_PLT0-18] + _ = x[R_PLT1-19] + _ = x[R_PLT2-20] + _ = x[R_USEFIELD-21] + _ = x[R_USETYPE-22] + _ = x[R_USEIFACE-23] + _ = x[R_USEIFACEMETHOD-24] + _ = x[R_USENAMEDMETHOD-25] + _ = x[R_METHODOFF-26] + _ = x[R_KEEP-27] + _ = x[R_POWER_TOC-28] + _ = x[R_GOTPCREL-29] + _ = x[R_JMPMIPS-30] + _ = x[R_DWARFSECREF-31] + _ = x[R_ARM64_TLS_LE-32] + _ = x[R_ARM64_TLS_IE-33] + _ = x[R_ARM64_GOTPCREL-34] + _ = x[R_ARM64_GOT-35] + _ = x[R_ARM64_PCREL-36] + _ = x[R_ARM64_PCREL_LDST8-37] + _ = x[R_ARM64_PCREL_LDST16-38] + _ = x[R_ARM64_PCREL_LDST32-39] + _ = x[R_ARM64_PCREL_LDST64-40] + _ = x[R_ARM64_LDST8-41] + _ = x[R_ARM64_LDST16-42] + _ = x[R_ARM64_LDST32-43] + _ = x[R_ARM64_LDST64-44] + _ = x[R_ARM64_LDST128-45] + _ = x[R_POWER_TLS_LE-46] + _ = x[R_POWER_TLS_IE-47] + _ = x[R_POWER_TLS-48] + _ = x[R_POWER_TLS_IE_PCREL34-49] + _ = x[R_POWER_TLS_LE_TPREL34-50] + _ = x[R_ADDRPOWER_DS-51] + _ = x[R_ADDRPOWER_GOT-52] + _ = x[R_ADDRPOWER_GOT_PCREL34-53] + _ = x[R_ADDRPOWER_PCREL-54] + _ = x[R_ADDRPOWER_TOCREL-55] + _ = x[R_ADDRPOWER_TOCREL_DS-56] + _ = x[R_ADDRPOWER_D34-57] + _ = x[R_ADDRPOWER_PCREL34-58] + _ = x[R_RISCV_JAL-59] + _ = x[R_RISCV_JAL_TRAMP-60] + _ = x[R_RISCV_CALL-61] + _ = x[R_RISCV_PCREL_ITYPE-62] + _ = x[R_RISCV_PCREL_STYPE-63] + _ = x[R_RISCV_TLS_IE-64] + _ = x[R_RISCV_TLS_LE-65] + _ = x[R_RISCV_GOT_HI20-66] + _ = x[R_RISCV_GOT_PCREL_ITYPE-67] + _ = x[R_RISCV_PCREL_HI20-68] + _ = x[R_RISCV_PCREL_LO12_I-69] + _ = x[R_RISCV_PCREL_LO12_S-70] + _ = x[R_RISCV_BRANCH-71] + _ = x[R_RISCV_ADD32-72] + _ = x[R_RISCV_SUB32-73] + _ = x[R_RISCV_RVC_BRANCH-74] + _ = x[R_RISCV_RVC_JUMP-75] + _ = x[R_PCRELDBL-76] + _ = x[R_LOONG64_ADDR_HI-77] + _ = x[R_LOONG64_ADDR_LO-78] + _ = x[R_LOONG64_ADDR_PCREL20_S2-79] + _ = x[R_LOONG64_TLS_LE_HI-80] + _ = x[R_LOONG64_TLS_LE_LO-81] + _ = x[R_CALLLOONG64-82] + _ = x[R_LOONG64_CALL36-83] + _ = x[R_LOONG64_TLS_IE_HI-84] + _ = x[R_LOONG64_TLS_IE_LO-85] + _ = x[R_LOONG64_GOT_HI-86] + _ = x[R_LOONG64_GOT_LO-87] + _ = x[R_LOONG64_ADD64-88] + _ = x[R_LOONG64_SUB64-89] + _ = x[R_JMP16LOONG64-90] + _ = x[R_JMP21LOONG64-91] + _ = x[R_JMPLOONG64-92] + _ = x[R_ADDRMIPSU-93] + _ = x[R_ADDRMIPSTLS-94] + _ = x[R_ADDRCUOFF-95] + _ = x[R_WASMIMPORT-96] + _ = x[R_XCOFFREF-97] + _ = x[R_PEIMAGEOFF-98] + _ = x[R_INITORDER-99] + _ = x[R_DWTXTADDR_U1-100] + _ = x[R_DWTXTADDR_U2-101] + _ = x[R_DWTXTADDR_U3-102] + _ = x[R_DWTXTADDR_U4-103] +} + +const _RelocType_name = "R_ADDRR_ADDRPOWERR_ADDRARM64R_ADDRMIPSR_ADDROFFR_SIZER_CALLR_CALLARMR_CALLARM64R_CALLINDR_CALLPOWERR_CALLMIPSR_CONSTR_PCRELR_TLS_LER_TLS_IER_GOTOFFR_PLT0R_PLT1R_PLT2R_USEFIELDR_USETYPER_USEIFACER_USEIFACEMETHODR_USENAMEDMETHODR_METHODOFFR_KEEPR_POWER_TOCR_GOTPCRELR_JMPMIPSR_DWARFSECREFR_ARM64_TLS_LER_ARM64_TLS_IER_ARM64_GOTPCRELR_ARM64_GOTR_ARM64_PCRELR_ARM64_PCREL_LDST8R_ARM64_PCREL_LDST16R_ARM64_PCREL_LDST32R_ARM64_PCREL_LDST64R_ARM64_LDST8R_ARM64_LDST16R_ARM64_LDST32R_ARM64_LDST64R_ARM64_LDST128R_POWER_TLS_LER_POWER_TLS_IER_POWER_TLSR_POWER_TLS_IE_PCREL34R_POWER_TLS_LE_TPREL34R_ADDRPOWER_DSR_ADDRPOWER_GOTR_ADDRPOWER_GOT_PCREL34R_ADDRPOWER_PCRELR_ADDRPOWER_TOCRELR_ADDRPOWER_TOCREL_DSR_ADDRPOWER_D34R_ADDRPOWER_PCREL34R_RISCV_JALR_RISCV_JAL_TRAMPR_RISCV_CALLR_RISCV_PCREL_ITYPER_RISCV_PCREL_STYPER_RISCV_TLS_IER_RISCV_TLS_LER_RISCV_GOT_HI20R_RISCV_GOT_PCREL_ITYPER_RISCV_PCREL_HI20R_RISCV_PCREL_LO12_IR_RISCV_PCREL_LO12_SR_RISCV_BRANCHR_RISCV_ADD32R_RISCV_SUB32R_RISCV_RVC_BRANCHR_RISCV_RVC_JUMPR_PCRELDBLR_LOONG64_ADDR_HIR_LOONG64_ADDR_LOR_LOONG64_ADDR_PCREL20_S2R_LOONG64_TLS_LE_HIR_LOONG64_TLS_LE_LOR_CALLLOONG64R_LOONG64_CALL36R_LOONG64_TLS_IE_HIR_LOONG64_TLS_IE_LOR_LOONG64_GOT_HIR_LOONG64_GOT_LOR_LOONG64_ADD64R_LOONG64_SUB64R_JMP16LOONG64R_JMP21LOONG64R_JMPLOONG64R_ADDRMIPSUR_ADDRMIPSTLSR_ADDRCUOFFR_WASMIMPORTR_XCOFFREFR_PEIMAGEOFFR_INITORDERR_DWTXTADDR_U1R_DWTXTADDR_U2R_DWTXTADDR_U3R_DWTXTADDR_U4" + +var _RelocType_index = [...]uint16{0, 6, 17, 28, 38, 47, 53, 59, 68, 79, 88, 99, 109, 116, 123, 131, 139, 147, 153, 159, 165, 175, 184, 194, 210, 226, 237, 243, 254, 264, 273, 286, 300, 314, 330, 341, 354, 373, 393, 413, 433, 446, 460, 474, 488, 503, 517, 531, 542, 564, 586, 600, 615, 638, 655, 673, 694, 709, 728, 739, 756, 768, 787, 806, 820, 834, 850, 873, 891, 911, 931, 945, 958, 971, 989, 1005, 1015, 1032, 1049, 1074, 1093, 1112, 1125, 1141, 1160, 1179, 1195, 1211, 1226, 1241, 1255, 1269, 1281, 1292, 1305, 1316, 1328, 1338, 1350, 1361, 1375, 1389, 1403, 1417} + +func (i RelocType) String() string { + i -= 1 + if i < 0 || i >= RelocType(len(_RelocType_index)-1) { + return "RelocType(" + strconv.FormatInt(int64(i+1), 10) + ")" + } + return _RelocType_name[_RelocType_index[i]:_RelocType_index[i+1]] +} diff --git a/internal/go/src/objabi/stack.go b/internal/go/src/objabi/stack.go new file mode 100755 index 0000000..c1b4464 --- /dev/null +++ b/internal/go/src/objabi/stack.go @@ -0,0 +1,32 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package objabi + +import ( + "loov.dev/lensm/internal/go/src/abi" + "loov.dev/lensm/internal/go/src/buildcfg" +) + +func StackNosplit(race bool) int { + // This arithmetic must match that in runtime/stack.go:stackNosplit. + return abi.StackNosplitBase * stackGuardMultiplier(race) +} + +// stackGuardMultiplier returns a multiplier to apply to the default +// stack guard size. Larger multipliers are used for non-optimized +// builds that have larger stack frames or for specific targets. +func stackGuardMultiplier(race bool) int { + // This arithmetic must match that in internal/runtime/sys/consts.go:StackGuardMultiplier. + n := 1 + // On AIX and OpenBSD, a larger stack is needed for syscalls. + if buildcfg.GOOS == "aix" || buildcfg.GOOS == "openbsd" { + n += 1 + } + // The race build also needs more stack. + if race { + n += 1 + } + return n +} diff --git a/internal/go/src/objabi/symkind.go b/internal/go/src/objabi/symkind.go new file mode 100755 index 0000000..155bdb9 --- /dev/null +++ b/internal/go/src/objabi/symkind.go @@ -0,0 +1,98 @@ +// Derived from Inferno utils/6l/l.h and related files. +// https://bitbucket.org/inferno-os/inferno-os/src/master/utils/6l/l.h +// +// Copyright © 1994-1999 Lucent Technologies Inc. All rights reserved. +// Portions Copyright © 1995-1997 C H Forsyth (forsyth@terzarima.net) +// Portions Copyright © 1997-1999 Vita Nuova Limited +// Portions Copyright © 2000-2007 Vita Nuova Holdings Limited (www.vitanuova.com) +// Portions Copyright © 2004,2006 Bruce Ellis +// Portions Copyright © 2005-2007 C H Forsyth (forsyth@terzarima.net) +// Revisions Copyright © 2000-2007 Lucent Technologies Inc. and others +// Portions Copyright © 2009 The Go Authors. All rights reserved. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +package objabi + +// A SymKind describes the kind of memory represented by a symbol. +type SymKind uint8 + +// Defined SymKind values. +// These are used to index into cmd/link/internal/sym/AbiSymKindToSymKind +// +// TODO(rsc): Give idiomatic Go names. +// +//go:generate stringer -type=SymKind +const ( + // An otherwise invalid zero value for the type + Sxxx SymKind = iota + // Executable instructions + STEXT + STEXTFIPS + // Read only static data + SRODATA + SRODATAFIPS + // Static data that does not contain any pointers + SNOPTRDATA + SNOPTRDATAFIPS + // Static data + SDATA + SDATAFIPS + // Statically data that is initially all 0s + SBSS + // Statically data that is initially all 0s and does not contain pointers + SNOPTRBSS + // Thread-local data that is initially all 0s + STLSBSS + // Debugging data + SDWARFCUINFO + SDWARFCONST + SDWARFFCN + SDWARFABSFCN + SDWARFTYPE + SDWARFVAR + SDWARFRANGE + SDWARFLOC + SDWARFLINES + SDWARFADDR + // Coverage instrumentation counter for libfuzzer. + SLIBFUZZER_8BIT_COUNTER + // Coverage instrumentation counter, aux variable for cmd/cover + SCOVERAGE_COUNTER + SCOVERAGE_AUXVAR + + SSEHUNWINDINFO + // Update cmd/link/internal/sym/AbiSymKindToSymKind for new SymKind values. +) + +// IsText reports whether t is one of the text kinds. +func (t SymKind) IsText() bool { + return t == STEXT || t == STEXTFIPS +} + +// IsDATA reports whether t is one of the DATA kinds (SDATA or SDATAFIPS, +// excluding NOPTRDATA, RODATA, BSS, and so on). +func (t SymKind) IsDATA() bool { + return t == SDATA || t == SDATAFIPS +} + +// IsFIPS reports whether t is one fo the FIPS kinds. +func (t SymKind) IsFIPS() bool { + return t == STEXTFIPS || t == SRODATAFIPS || t == SNOPTRDATAFIPS || t == SDATAFIPS +} diff --git a/internal/go/src/objabi/symkind_string.go b/internal/go/src/objabi/symkind_string.go new file mode 100755 index 0000000..fbf6a82 --- /dev/null +++ b/internal/go/src/objabi/symkind_string.go @@ -0,0 +1,48 @@ +// Code generated by "stringer -type=SymKind"; DO NOT EDIT. + +package objabi + +import "strconv" + +func _() { + // An "invalid array index" compiler error signifies that the constant values have changed. + // Re-run the stringer command to generate them again. + var x [1]struct{} + _ = x[Sxxx-0] + _ = x[STEXT-1] + _ = x[STEXTFIPS-2] + _ = x[SRODATA-3] + _ = x[SRODATAFIPS-4] + _ = x[SNOPTRDATA-5] + _ = x[SNOPTRDATAFIPS-6] + _ = x[SDATA-7] + _ = x[SDATAFIPS-8] + _ = x[SBSS-9] + _ = x[SNOPTRBSS-10] + _ = x[STLSBSS-11] + _ = x[SDWARFCUINFO-12] + _ = x[SDWARFCONST-13] + _ = x[SDWARFFCN-14] + _ = x[SDWARFABSFCN-15] + _ = x[SDWARFTYPE-16] + _ = x[SDWARFVAR-17] + _ = x[SDWARFRANGE-18] + _ = x[SDWARFLOC-19] + _ = x[SDWARFLINES-20] + _ = x[SDWARFADDR-21] + _ = x[SLIBFUZZER_8BIT_COUNTER-22] + _ = x[SCOVERAGE_COUNTER-23] + _ = x[SCOVERAGE_AUXVAR-24] + _ = x[SSEHUNWINDINFO-25] +} + +const _SymKind_name = "SxxxSTEXTSTEXTFIPSSRODATASRODATAFIPSSNOPTRDATASNOPTRDATAFIPSSDATASDATAFIPSSBSSSNOPTRBSSSTLSBSSSDWARFCUINFOSDWARFCONSTSDWARFFCNSDWARFABSFCNSDWARFTYPESDWARFVARSDWARFRANGESDWARFLOCSDWARFLINESSDWARFADDRSLIBFUZZER_8BIT_COUNTERSCOVERAGE_COUNTERSCOVERAGE_AUXVARSSEHUNWINDINFO" + +var _SymKind_index = [...]uint16{0, 4, 9, 18, 25, 36, 46, 60, 65, 74, 78, 87, 94, 106, 117, 126, 138, 148, 157, 168, 177, 188, 198, 221, 238, 254, 268} + +func (i SymKind) String() string { + if i >= SymKind(len(_SymKind_index)-1) { + return "SymKind(" + strconv.FormatInt(int64(i), 10) + ")" + } + return _SymKind_name[_SymKind_index[i]:_SymKind_index[i+1]] +} diff --git a/internal/go/src/objabi/util.go b/internal/go/src/objabi/util.go new file mode 100755 index 0000000..9995020 --- /dev/null +++ b/internal/go/src/objabi/util.go @@ -0,0 +1,33 @@ +// Copyright 2015 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package objabi + +import ( + "fmt" + "strings" + + "loov.dev/lensm/internal/go/src/buildcfg" +) + +const ( + ElfRelocOffset = 256 + MachoRelocOffset = 2048 // reserve enough space for ELF relocations + GlobalDictPrefix = ".dict" // prefix for names of global dictionaries +) + +// HeaderString returns the toolchain configuration string written in +// Go object headers. This string ensures we don't attempt to import +// or link object files that are incompatible with each other. This +// string always starts with "go object ". +func HeaderString() string { + archExtra := "" + if k, v := buildcfg.GOGOARCH(); k != "" && v != "" { + archExtra = " " + k + "=" + v + } + return fmt.Sprintf("go object %s %s %s%s X:%s\n", + buildcfg.GOOS, buildcfg.GOARCH, + buildcfg.Version, archExtra, + strings.Join(buildcfg.Experiment.Enabled(), ",")) +} diff --git a/internal/go/src/objabi/zbootstrap.go b/internal/go/src/objabi/zbootstrap.go new file mode 100755 index 0000000..6cfb5a6 --- /dev/null +++ b/internal/go/src/objabi/zbootstrap.go @@ -0,0 +1,3 @@ +// Code generated by go tool dist; DO NOT EDIT. + +package objabi diff --git a/internal/go/src/objfile/elf.go b/internal/go/src/objfile/elf.go new file mode 100755 index 0000000..6988cea --- /dev/null +++ b/internal/go/src/objfile/elf.go @@ -0,0 +1,174 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Parsing of ELF executables (Linux, FreeBSD, and so on). + +package objfile + +import ( + "debug/dwarf" + "debug/elf" + "encoding/binary" + "fmt" + "io" +) + +type elfFile struct { + elf *elf.File +} + +func openElf(r io.ReaderAt) (rawFile, error) { + f, err := elf.NewFile(r) + if err != nil { + return nil, err + } + return &elfFile{f}, nil +} + +func (f *elfFile) symbols() ([]Sym, error) { + elfSyms, err := f.elf.Symbols() + if err != nil { + return nil, err + } + + var syms []Sym + for _, s := range elfSyms { + sym := Sym{Addr: s.Value, Name: s.Name, Size: int64(s.Size), Code: '?'} + switch s.Section { + case elf.SHN_UNDEF: + sym.Code = 'U' + case elf.SHN_COMMON: + sym.Code = 'B' + default: + i := int(s.Section) + if i < 0 || i >= len(f.elf.Sections) { + break + } + sect := f.elf.Sections[i] + switch sect.Flags & (elf.SHF_WRITE | elf.SHF_ALLOC | elf.SHF_EXECINSTR) { + case elf.SHF_ALLOC | elf.SHF_EXECINSTR: + sym.Code = 'T' + case elf.SHF_ALLOC: + sym.Code = 'R' + case elf.SHF_ALLOC | elf.SHF_WRITE: + sym.Code = 'D' + } + } + if elf.ST_BIND(s.Info) == elf.STB_LOCAL { + sym.Code += 'a' - 'A' + } + syms = append(syms, sym) + } + + return syms, nil +} + +func (f *elfFile) pcln() (textStart uint64, pclntab []byte, err error) { + if sect := f.elf.Section(".text"); sect != nil { + textStart = sect.Addr + } + + sect := f.elf.Section(".gopclntab") + if sect == nil { + // try .data.rel.ro.gopclntab, for PIE binaries + sect = f.elf.Section(".data.rel.ro.gopclntab") + } + if sect != nil { + if pclntab, err = sect.Data(); err != nil { + return 0, nil, err + } + } else { + // if both sections failed, try the symbol + pclntab = f.symbolData("runtime.pclntab", "runtime.epclntab") + } + + return textStart, pclntab, nil +} + +func (f *elfFile) text() (textStart uint64, text []byte, err error) { + sect := f.elf.Section(".text") + if sect == nil { + return 0, nil, fmt.Errorf("text section not found") + } + textStart = sect.Addr + text, err = sect.Data() + return +} + +func (f *elfFile) goarch() string { + switch f.elf.Machine { + case elf.EM_386: + return "386" + case elf.EM_X86_64: + return "amd64" + case elf.EM_ARM: + return "arm" + case elf.EM_AARCH64: + return "arm64" + case elf.EM_LOONGARCH: + return "loong64" + case elf.EM_PPC64: + if f.elf.ByteOrder == binary.LittleEndian { + return "ppc64le" + } + return "ppc64" + case elf.EM_RISCV: + if f.elf.Class == elf.ELFCLASS64 { + return "riscv64" + } + case elf.EM_S390: + return "s390x" + } + return "" +} + +func (f *elfFile) loadAddress() (uint64, error) { + for _, p := range f.elf.Progs { + if p.Type == elf.PT_LOAD && p.Flags&elf.PF_X != 0 { + // The memory mapping that contains the segment + // starts at an aligned address. Apparently this + // is what pprof expects, as it uses this and the + // start address of the mapping to compute PC + // delta. + return p.Vaddr - p.Vaddr%p.Align, nil + } + } + return 0, fmt.Errorf("unknown load address") +} + +func (f *elfFile) dwarf() (*dwarf.Data, error) { + return f.elf.DWARF() +} + +func (f *elfFile) symbolData(start, end string) []byte { + elfSyms, err := f.elf.Symbols() + if err != nil { + return nil + } + var addr, eaddr uint64 + for _, s := range elfSyms { + if s.Name == start { + addr = s.Value + } else if s.Name == end { + eaddr = s.Value + } + if addr != 0 && eaddr != 0 { + break + } + } + if addr == 0 || eaddr < addr { + return nil + } + size := eaddr - addr + data := make([]byte, size) + for _, prog := range f.elf.Progs { + if prog.Vaddr <= addr && addr+size-1 <= prog.Vaddr+prog.Filesz-1 { + if _, err := prog.ReadAt(data, int64(addr-prog.Vaddr)); err != nil { + return nil + } + return data + } + } + return nil +} diff --git a/internal/go/src/objfile/goobj.go b/internal/go/src/objfile/goobj.go new file mode 100755 index 0000000..9327189 --- /dev/null +++ b/internal/go/src/objfile/goobj.go @@ -0,0 +1,342 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Parsing of Go intermediate object files and archives. + +package objfile + +import ( + "loov.dev/lensm/internal/go/src/archive" + "loov.dev/lensm/internal/go/src/goobj" + "loov.dev/lensm/internal/go/src/objabi" + "loov.dev/lensm/internal/go/src/sys" + "debug/dwarf" + "debug/gosym" + "errors" + "fmt" + "io" + "os" +) + +type goobjFile struct { + goobj *archive.GoObj + r *goobj.Reader + f *os.File + arch *sys.Arch +} + +func openGoFile(f *os.File) (*File, error) { + a, err := archive.Parse(f, false) + if err != nil { + return nil, err + } + entries := make([]*Entry, 0, len(a.Entries)) +L: + for _, e := range a.Entries { + switch e.Type { + case archive.EntryPkgDef, archive.EntrySentinelNonObj: + continue + case archive.EntryGoObj: + o := e.Obj + b := make([]byte, o.Size) + _, err := f.ReadAt(b, o.Offset) + if err != nil { + return nil, err + } + r := goobj.NewReaderFromBytes(b, false) + var arch *sys.Arch + for _, a := range sys.Archs { + if a.Name == e.Obj.Arch { + arch = a + break + } + } + entries = append(entries, &Entry{ + name: e.Name, + raw: &goobjFile{e.Obj, r, f, arch}, + }) + continue + case archive.EntryNativeObj: + nr := io.NewSectionReader(f, e.Offset, e.Size) + for _, try := range openers { + if raw, err := try(nr); err == nil { + entries = append(entries, &Entry{ + name: e.Name, + raw: raw, + }) + continue L + } + } + } + return nil, fmt.Errorf("open %s: unrecognized archive member %s", f.Name(), e.Name) + } + return &File{f, entries}, nil +} + +func goobjName(name string, ver int) string { + if ver == 0 { + return name + } + return fmt.Sprintf("%s<%d>", name, ver) +} + +type goobjReloc struct { + Off int32 + Size uint8 + Type objabi.RelocType + Add int64 + Sym string +} + +func (r goobjReloc) String(insnOffset uint64) string { + delta := int64(r.Off) - int64(insnOffset) + s := fmt.Sprintf("[%d:%d]%s", delta, delta+int64(r.Size), r.Type) + if r.Sym != "" { + if r.Add != 0 { + return fmt.Sprintf("%s:%s+%d", s, r.Sym, r.Add) + } + return fmt.Sprintf("%s:%s", s, r.Sym) + } + if r.Add != 0 { + return fmt.Sprintf("%s:%d", s, r.Add) + } + return s +} + +func (f *goobjFile) symbols() ([]Sym, error) { + r := f.r + var syms []Sym + + // Name of referenced indexed symbols. + nrefName := r.NRefName() + refNames := make(map[goobj.SymRef]string, nrefName) + for i := 0; i < nrefName; i++ { + rn := r.RefName(i) + refNames[rn.Sym()] = rn.Name(r) + } + + abiToVer := func(abi uint16) int { + var ver int + if abi == goobj.SymABIstatic { + // Static symbol + ver = 1 + } + return ver + } + + resolveSymRef := func(s goobj.SymRef) string { + var i uint32 + switch p := s.PkgIdx; p { + case goobj.PkgIdxInvalid: + if s.SymIdx != 0 { + panic("bad sym ref") + } + return "" + case goobj.PkgIdxHashed64: + i = s.SymIdx + uint32(r.NSym()) + case goobj.PkgIdxHashed: + i = s.SymIdx + uint32(r.NSym()+r.NHashed64def()) + case goobj.PkgIdxNone: + i = s.SymIdx + uint32(r.NSym()+r.NHashed64def()+r.NHasheddef()) + case goobj.PkgIdxBuiltin: + name, abi := goobj.BuiltinName(int(s.SymIdx)) + return goobjName(name, abi) + case goobj.PkgIdxSelf: + i = s.SymIdx + default: + return refNames[s] + } + sym := r.Sym(i) + return goobjName(sym.Name(r), abiToVer(sym.ABI())) + } + + // Defined symbols + ndef := uint32(r.NSym() + r.NHashed64def() + r.NHasheddef() + r.NNonpkgdef()) + for i := uint32(0); i < ndef; i++ { + osym := r.Sym(i) + if osym.Name(r) == "" { + continue // not a real symbol + } + name := osym.Name(r) + ver := osym.ABI() + name = goobjName(name, abiToVer(ver)) + typ := objabi.SymKind(osym.Type()) + var code rune = '?' + switch typ { + case objabi.STEXT, objabi.STEXTFIPS: + code = 'T' + case objabi.SRODATA, objabi.SRODATAFIPS: + code = 'R' + case objabi.SNOPTRDATA, objabi.SNOPTRDATAFIPS, + objabi.SDATA, objabi.SDATAFIPS: + code = 'D' + case objabi.SBSS, objabi.SNOPTRBSS, objabi.STLSBSS: + code = 'B' + } + if ver >= goobj.SymABIstatic { + code += 'a' - 'A' + } + + sym := Sym{ + Name: name, + Addr: uint64(r.DataOff(i)), + Size: int64(osym.Siz()), + Code: code, + } + + relocs := r.Relocs(i) + sym.Relocs = make([]Reloc, len(relocs)) + for j := range relocs { + rel := &relocs[j] + sym.Relocs[j] = Reloc{ + Addr: uint64(r.DataOff(i)) + uint64(rel.Off()), + Size: uint64(rel.Siz()), + Stringer: goobjReloc{ + Off: rel.Off(), + Size: rel.Siz(), + Type: objabi.RelocType(rel.Type()), + Add: rel.Add(), + Sym: resolveSymRef(rel.Sym()), + }, + } + } + + syms = append(syms, sym) + } + + // Referenced symbols + n := ndef + uint32(r.NNonpkgref()) + for i := ndef; i < n; i++ { + osym := r.Sym(i) + sym := Sym{Name: osym.Name(r), Code: 'U'} + syms = append(syms, sym) + } + for i := 0; i < nrefName; i++ { + rn := r.RefName(i) + sym := Sym{Name: rn.Name(r), Code: 'U'} + syms = append(syms, sym) + } + + return syms, nil +} + +func (f *goobjFile) pcln() (textStart uint64, pclntab []byte, err error) { + // Should never be called. We implement Liner below, callers + // should use that instead. + return 0, nil, fmt.Errorf("pcln not available in go object file") +} + +// PCToLine returns the file name, line, and function data for the given pc. +// Returns "",0,nil if unknown. +// This function implements the Liner interface in preference to pcln() above. +func (f *goobjFile) PCToLine(pc uint64) (string, int, *gosym.Func) { + r := f.r + if f.arch == nil { + return "", 0, nil + } + getSymData := func(s goobj.SymRef) []byte { + if s.PkgIdx != goobj.PkgIdxHashed { + // We don't need the data for non-hashed symbols, yet. + panic("not supported") + } + i := s.SymIdx + uint32(r.NSym()+r.NHashed64def()) + return r.BytesAt(r.DataOff(i), r.DataSize(i)) + } + + ndef := uint32(r.NSym() + r.NHashed64def() + r.NHasheddef() + r.NNonpkgdef()) + for i := uint32(0); i < ndef; i++ { + osym := r.Sym(i) + addr := uint64(r.DataOff(i)) + if pc < addr || pc >= addr+uint64(osym.Siz()) { + continue + } + var pcfileSym, pclineSym goobj.SymRef + for _, a := range r.Auxs(i) { + switch a.Type() { + case goobj.AuxPcfile: + pcfileSym = a.Sym() + case goobj.AuxPcline: + pclineSym = a.Sym() + } + } + if pcfileSym.IsZero() || pclineSym.IsZero() { + continue + } + pcline := getSymData(pclineSym) + line := int(pcValue(pcline, pc-addr, f.arch)) + pcfile := getSymData(pcfileSym) + fileID := pcValue(pcfile, pc-addr, f.arch) + fileName := r.File(int(fileID)) + // Note: we provide only the name in the Func structure. + // We could provide more if needed. + return fileName, line, &gosym.Func{Sym: &gosym.Sym{Name: osym.Name(r)}} + } + return "", 0, nil +} + +// pcValue looks up the given PC in a pc value table. target is the +// offset of the pc from the entry point. +func pcValue(tab []byte, target uint64, arch *sys.Arch) int32 { + val := int32(-1) + var pc uint64 + for step(&tab, &pc, &val, pc == 0, arch) { + if target < pc { + return val + } + } + return -1 +} + +// step advances to the next pc, value pair in the encoded table. +func step(p *[]byte, pc *uint64, val *int32, first bool, arch *sys.Arch) bool { + uvdelta := readvarint(p) + if uvdelta == 0 && !first { + return false + } + if uvdelta&1 != 0 { + uvdelta = ^(uvdelta >> 1) + } else { + uvdelta >>= 1 + } + vdelta := int32(uvdelta) + pcdelta := readvarint(p) * uint32(arch.MinLC) + *pc += uint64(pcdelta) + *val += vdelta + return true +} + +// readvarint reads, removes, and returns a varint from *p. +func readvarint(p *[]byte) uint32 { + var v, shift uint32 + s := *p + for shift = 0; ; shift += 7 { + b := s[0] + s = s[1:] + v |= (uint32(b) & 0x7F) << shift + if b&0x80 == 0 { + break + } + } + *p = s + return v +} + +// We treat the whole object file as the text section. +func (f *goobjFile) text() (textStart uint64, text []byte, err error) { + text = make([]byte, f.goobj.Size) + _, err = f.f.ReadAt(text, f.goobj.Offset) + return +} + +func (f *goobjFile) goarch() string { + return f.goobj.Arch +} + +func (f *goobjFile) loadAddress() (uint64, error) { + return 0, fmt.Errorf("unknown load address") +} + +func (f *goobjFile) dwarf() (*dwarf.Data, error) { + return nil, errors.New("no DWARF data in go object file") +} diff --git a/internal/go/src/objfile/macho.go b/internal/go/src/objfile/macho.go new file mode 100755 index 0000000..eaf665f --- /dev/null +++ b/internal/go/src/objfile/macho.go @@ -0,0 +1,129 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Parsing of Mach-O executables (OS X). + +package objfile + +import ( + "debug/dwarf" + "debug/macho" + "fmt" + "io" + "slices" + "sort" +) + +const stabTypeMask = 0xe0 + +type machoFile struct { + macho *macho.File +} + +func openMacho(r io.ReaderAt) (rawFile, error) { + f, err := macho.NewFile(r) + if err != nil { + return nil, err + } + return &machoFile{f}, nil +} + +func (f *machoFile) symbols() ([]Sym, error) { + if f.macho.Symtab == nil { + return nil, nil + } + + // Build sorted list of addresses of all symbols. + // We infer the size of a symbol by looking at where the next symbol begins. + var addrs []uint64 + for _, s := range f.macho.Symtab.Syms { + // Skip stab debug info. + if s.Type&stabTypeMask == 0 { + addrs = append(addrs, s.Value) + } + } + slices.Sort(addrs) + + var syms []Sym + for _, s := range f.macho.Symtab.Syms { + if s.Type&stabTypeMask != 0 { + // Skip stab debug info. + continue + } + sym := Sym{Name: s.Name, Addr: s.Value, Code: '?'} + i := sort.Search(len(addrs), func(x int) bool { return addrs[x] > s.Value }) + if i < len(addrs) { + sym.Size = int64(addrs[i] - s.Value) + } + if s.Sect == 0 { + sym.Code = 'U' + } else if int(s.Sect) <= len(f.macho.Sections) { + sect := f.macho.Sections[s.Sect-1] + switch sect.Seg { + case "__TEXT", "__DATA_CONST": + sym.Code = 'R' + case "__DATA": + sym.Code = 'D' + } + switch sect.Seg + " " + sect.Name { + case "__TEXT __text": + sym.Code = 'T' + case "__DATA __bss", "__DATA __noptrbss": + sym.Code = 'B' + } + } + syms = append(syms, sym) + } + + return syms, nil +} + +func (f *machoFile) pcln() (textStart uint64, pclntab []byte, err error) { + if sect := f.macho.Section("__text"); sect != nil { + textStart = sect.Addr + } + if sect := f.macho.Section("__gopclntab"); sect != nil { + if pclntab, err = sect.Data(); err != nil { + return 0, nil, err + } + } + return textStart, pclntab, nil +} + +func (f *machoFile) text() (textStart uint64, text []byte, err error) { + sect := f.macho.Section("__text") + if sect == nil { + return 0, nil, fmt.Errorf("text section not found") + } + textStart = sect.Addr + text, err = sect.Data() + return +} + +func (f *machoFile) goarch() string { + switch f.macho.Cpu { + case macho.Cpu386: + return "386" + case macho.CpuAmd64: + return "amd64" + case macho.CpuArm: + return "arm" + case macho.CpuArm64: + return "arm64" + case macho.CpuPpc64: + return "ppc64" + } + return "" +} + +func (f *machoFile) loadAddress() (uint64, error) { + if seg := f.macho.Segment("__TEXT"); seg != nil { + return seg.Addr, nil + } + return 0, fmt.Errorf("unknown load address") +} + +func (f *machoFile) dwarf() (*dwarf.Data, error) { + return f.macho.DWARF() +} diff --git a/internal/go/src/objfile/objfile.go b/internal/go/src/objfile/objfile.go new file mode 100755 index 0000000..5cc0943 --- /dev/null +++ b/internal/go/src/objfile/objfile.go @@ -0,0 +1,185 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package objfile implements portable access to OS-specific executable files. +package objfile + +import ( + "loov.dev/lensm/internal/go/src/archive" + "cmp" + "debug/dwarf" + "debug/gosym" + "fmt" + "io" + "os" + "slices" +) + +type rawFile interface { + symbols() (syms []Sym, err error) + pcln() (textStart uint64, pclntab []byte, err error) + text() (textStart uint64, text []byte, err error) + goarch() string + loadAddress() (uint64, error) + dwarf() (*dwarf.Data, error) +} + +// A File is an opened executable file. +type File struct { + r *os.File + entries []*Entry +} + +type Entry struct { + name string + raw rawFile +} + +// A Sym is a symbol defined in an executable file. +type Sym struct { + Name string // symbol name + Addr uint64 // virtual address of symbol + Size int64 // size in bytes + Code rune // nm code (T for text, D for data, and so on) + Type string // XXX? + Relocs []Reloc // in increasing Addr order +} + +type Reloc struct { + Addr uint64 // Address of first byte that reloc applies to. + Size uint64 // Number of bytes + Stringer RelocStringer +} + +type RelocStringer interface { + // insnOffset is the offset of the instruction containing the relocation + // from the start of the symbol containing the relocation. + String(insnOffset uint64) string +} + +var openers = []func(io.ReaderAt) (rawFile, error){ + openElf, + openMacho, + openPE, + openPlan9, + openXcoff, +} + +// Open opens the named file. +// The caller must call f.Close when the file is no longer needed. +func Open(name string) (*File, error) { + r, err := os.Open(name) + if err != nil { + return nil, err + } + if f, err := openGoFile(r); err == nil { + return f, nil + } else if _, ok := err.(archive.ErrGoObjOtherVersion); ok { + return nil, fmt.Errorf("open %s: %v", name, err) + } + for _, try := range openers { + if raw, err := try(r); err == nil { + return &File{r, []*Entry{{raw: raw}}}, nil + } + } + r.Close() + return nil, fmt.Errorf("open %s: unrecognized object file", name) +} + +func (f *File) Close() error { + return f.r.Close() +} + +func (f *File) Entries() []*Entry { + return f.entries +} + +func (f *File) Symbols() ([]Sym, error) { + return f.entries[0].Symbols() +} + +func (f *File) PCLineTable() (Liner, error) { + return f.entries[0].PCLineTable() +} + +func (f *File) Text() (uint64, []byte, error) { + return f.entries[0].Text() +} + +func (f *File) GOARCH() string { + return f.entries[0].GOARCH() +} + +func (f *File) LoadAddress() (uint64, error) { + return f.entries[0].LoadAddress() +} + +func (f *File) DWARF() (*dwarf.Data, error) { + return f.entries[0].DWARF() +} + +func (e *Entry) Name() string { + return e.name +} + +func (e *Entry) Symbols() ([]Sym, error) { + syms, err := e.raw.symbols() + if err != nil { + return nil, err + } + slices.SortFunc(syms, func(a, b Sym) int { + return cmp.Compare(a.Addr, b.Addr) + }) + return syms, nil +} + +func (e *Entry) PCLineTable() (Liner, error) { + // If the raw file implements Liner directly, use that. + // Currently, only Go intermediate objects and archives (goobj) use this path. + if pcln, ok := e.raw.(Liner); ok { + return pcln, nil + } + // Otherwise, read the pcln tables and build a Liner out of that. + textStart, pclntab, err := e.raw.pcln() + if err != nil { + return nil, err + } + syms, err := e.raw.symbols() + if err == nil { + for _, s := range syms { + if s.Name == "runtime.text" { + textStart = s.Addr + break + } + } + } + return gosym.NewTable(nil, gosym.NewLineTable(pclntab, textStart)) +} + +func (e *Entry) Text() (uint64, []byte, error) { + return e.raw.text() +} + +func (e *Entry) GOARCH() string { + return e.raw.goarch() +} + +// LoadAddress returns the expected load address of the file. +// This differs from the actual load address for a position-independent +// executable. +func (e *Entry) LoadAddress() (uint64, error) { + return e.raw.loadAddress() +} + +// DWARF returns DWARF debug data for the file, if any. +// This is for cmd/pprof to locate cgo functions. +func (e *Entry) DWARF() (*dwarf.Data, error) { + return e.raw.dwarf() +} + +type Liner interface { + // Given a pc, returns the corresponding file, line, and function data. + // If unknown, returns "",0,nil. + PCToLine(uint64) (string, int, *gosym.Func) +} diff --git a/internal/go/src/objfile/pe.go b/internal/go/src/objfile/pe.go new file mode 100755 index 0000000..e948212 --- /dev/null +++ b/internal/go/src/objfile/pe.go @@ -0,0 +1,194 @@ +// Copyright 2013 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Parsing of PE executables (Microsoft Windows). + +package objfile + +import ( + "debug/dwarf" + "debug/pe" + "fmt" + "io" + "slices" + "sort" +) + +type peFile struct { + pe *pe.File +} + +func openPE(r io.ReaderAt) (rawFile, error) { + f, err := pe.NewFile(r) + if err != nil { + return nil, err + } + return &peFile{f}, nil +} + +func (f *peFile) symbols() ([]Sym, error) { + // Build sorted list of addresses of all symbols. + // We infer the size of a symbol by looking at where the next symbol begins. + var addrs []uint64 + + imageBase, _ := f.imageBase() + + var syms []Sym + for _, s := range f.pe.Symbols { + const ( + N_UNDEF = 0 // An undefined (extern) symbol + N_ABS = -1 // An absolute symbol (e_value is a constant, not an address) + N_DEBUG = -2 // A debugging symbol + ) + sym := Sym{Name: s.Name, Addr: uint64(s.Value), Code: '?'} + switch s.SectionNumber { + case N_UNDEF: + sym.Code = 'U' + case N_ABS: + sym.Code = 'C' + case N_DEBUG: + sym.Code = '?' + default: + if s.SectionNumber < 0 || len(f.pe.Sections) < int(s.SectionNumber) { + return nil, fmt.Errorf("invalid section number in symbol table") + } + sect := f.pe.Sections[s.SectionNumber-1] + const ( + text = 0x20 + data = 0x40 + bss = 0x80 + permW = 0x80000000 + ) + ch := sect.Characteristics + switch { + case ch&text != 0: + sym.Code = 'T' + case ch&data != 0: + if ch&permW == 0 { + sym.Code = 'R' + } else { + sym.Code = 'D' + } + case ch&bss != 0: + sym.Code = 'B' + } + sym.Addr += imageBase + uint64(sect.VirtualAddress) + } + syms = append(syms, sym) + addrs = append(addrs, sym.Addr) + } + + slices.Sort(addrs) + for i := range syms { + j := sort.Search(len(addrs), func(x int) bool { return addrs[x] > syms[i].Addr }) + if j < len(addrs) { + syms[i].Size = int64(addrs[j] - syms[i].Addr) + } + } + + return syms, nil +} + +func (f *peFile) pcln() (textStart uint64, pclntab []byte, err error) { + imageBase, err := f.imageBase() + if err != nil { + return 0, nil, err + } + + if sect := f.pe.Section(".text"); sect != nil { + textStart = imageBase + uint64(sect.VirtualAddress) + } + if pclntab, err = loadPETable(f.pe, "runtime.pclntab", "runtime.epclntab"); err != nil { + // We didn't find the symbols, so look for the names used in 1.3 and earlier. + // TODO: Remove code looking for the old symbols when we no longer care about 1.3. + var err2 error + if pclntab, err2 = loadPETable(f.pe, "pclntab", "epclntab"); err2 != nil { + return 0, nil, err + } + } + return textStart, pclntab, nil +} + +func (f *peFile) text() (textStart uint64, text []byte, err error) { + imageBase, err := f.imageBase() + if err != nil { + return 0, nil, err + } + + sect := f.pe.Section(".text") + if sect == nil { + return 0, nil, fmt.Errorf("text section not found") + } + textStart = imageBase + uint64(sect.VirtualAddress) + text, err = sect.Data() + return +} + +func findPESymbol(f *pe.File, name string) (*pe.Symbol, error) { + for _, s := range f.Symbols { + if s.Name != name { + continue + } + if s.SectionNumber <= 0 { + return nil, fmt.Errorf("symbol %s: invalid section number %d", name, s.SectionNumber) + } + if len(f.Sections) < int(s.SectionNumber) { + return nil, fmt.Errorf("symbol %s: section number %d is larger than max %d", name, s.SectionNumber, len(f.Sections)) + } + return s, nil + } + return nil, fmt.Errorf("no %s symbol found", name) +} + +func loadPETable(f *pe.File, sname, ename string) ([]byte, error) { + ssym, err := findPESymbol(f, sname) + if err != nil { + return nil, err + } + esym, err := findPESymbol(f, ename) + if err != nil { + return nil, err + } + if ssym.SectionNumber != esym.SectionNumber { + return nil, fmt.Errorf("%s and %s symbols must be in the same section", sname, ename) + } + sect := f.Sections[ssym.SectionNumber-1] + data, err := sect.Data() + if err != nil { + return nil, err + } + return data[ssym.Value:esym.Value], nil +} + +func (f *peFile) goarch() string { + switch f.pe.Machine { + case pe.IMAGE_FILE_MACHINE_I386: + return "386" + case pe.IMAGE_FILE_MACHINE_AMD64: + return "amd64" + case pe.IMAGE_FILE_MACHINE_ARM64: + return "arm64" + default: + return "" + } +} + +func (f *peFile) loadAddress() (uint64, error) { + return f.imageBase() +} + +func (f *peFile) imageBase() (uint64, error) { + switch oh := f.pe.OptionalHeader.(type) { + case *pe.OptionalHeader32: + return uint64(oh.ImageBase), nil + case *pe.OptionalHeader64: + return oh.ImageBase, nil + default: + return 0, fmt.Errorf("pe file format not recognized") + } +} + +func (f *peFile) dwarf() (*dwarf.Data, error) { + return f.pe.DWARF() +} diff --git a/internal/go/src/objfile/plan9obj.go b/internal/go/src/objfile/plan9obj.go new file mode 100755 index 0000000..edd4023 --- /dev/null +++ b/internal/go/src/objfile/plan9obj.go @@ -0,0 +1,150 @@ +// Copyright 2014 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Parsing of Plan 9 a.out executables. + +package objfile + +import ( + "debug/dwarf" + "debug/plan9obj" + "errors" + "fmt" + "io" + "slices" + "sort" +) + +var validSymType = map[rune]bool{ + 'T': true, + 't': true, + 'D': true, + 'd': true, + 'B': true, + 'b': true, +} + +type plan9File struct { + plan9 *plan9obj.File +} + +func openPlan9(r io.ReaderAt) (rawFile, error) { + f, err := plan9obj.NewFile(r) + if err != nil { + return nil, err + } + return &plan9File{f}, nil +} + +func (f *plan9File) symbols() ([]Sym, error) { + plan9Syms, err := f.plan9.Symbols() + if err != nil { + return nil, err + } + + // Build sorted list of addresses of all symbols. + // We infer the size of a symbol by looking at where the next symbol begins. + var addrs []uint64 + for _, s := range plan9Syms { + if !validSymType[s.Type] { + continue + } + addrs = append(addrs, s.Value) + } + slices.Sort(addrs) + + var syms []Sym + + for _, s := range plan9Syms { + if !validSymType[s.Type] { + continue + } + sym := Sym{Addr: s.Value, Name: s.Name, Code: s.Type} + i := sort.Search(len(addrs), func(x int) bool { return addrs[x] > s.Value }) + if i < len(addrs) { + sym.Size = int64(addrs[i] - s.Value) + } + syms = append(syms, sym) + } + + return syms, nil +} + +func (f *plan9File) pcln() (textStart uint64, pclntab []byte, err error) { + textStart = f.plan9.LoadAddress + f.plan9.HdrSize + if pclntab, err = loadPlan9Table(f.plan9, "runtime.pclntab", "runtime.epclntab"); err != nil { + // We didn't find the symbols, so look for the names used in 1.3 and earlier. + // TODO: Remove code looking for the old symbols when we no longer care about 1.3. + var err2 error + if pclntab, err2 = loadPlan9Table(f.plan9, "pclntab", "epclntab"); err2 != nil { + return 0, nil, err + } + } + return textStart, pclntab, nil +} + +func (f *plan9File) text() (textStart uint64, text []byte, err error) { + sect := f.plan9.Section("text") + if sect == nil { + return 0, nil, fmt.Errorf("text section not found") + } + textStart = f.plan9.LoadAddress + f.plan9.HdrSize + text, err = sect.Data() + return +} + +func findPlan9Symbol(f *plan9obj.File, name string) (*plan9obj.Sym, error) { + syms, err := f.Symbols() + if err != nil { + return nil, err + } + for _, s := range syms { + if s.Name != name { + continue + } + return &s, nil + } + return nil, fmt.Errorf("no %s symbol found", name) +} + +func loadPlan9Table(f *plan9obj.File, sname, ename string) ([]byte, error) { + ssym, err := findPlan9Symbol(f, sname) + if err != nil { + return nil, err + } + esym, err := findPlan9Symbol(f, ename) + if err != nil { + return nil, err + } + sect := f.Section("text") + if sect == nil { + return nil, err + } + data, err := sect.Data() + if err != nil { + return nil, err + } + textStart := f.LoadAddress + f.HdrSize + return data[ssym.Value-textStart : esym.Value-textStart], nil +} + +func (f *plan9File) goarch() string { + switch f.plan9.Magic { + case plan9obj.Magic386: + return "386" + case plan9obj.MagicAMD64: + return "amd64" + case plan9obj.MagicARM: + return "arm" + } + return "" +} + +func (f *plan9File) loadAddress() (uint64, error) { + return 0, fmt.Errorf("unknown load address") +} + +func (f *plan9File) dwarf() (*dwarf.Data, error) { + return nil, errors.New("no DWARF data in Plan 9 file") +} diff --git a/internal/go/src/objfile/xcoff.go b/internal/go/src/objfile/xcoff.go new file mode 100755 index 0000000..b3f4265 --- /dev/null +++ b/internal/go/src/objfile/xcoff.go @@ -0,0 +1,162 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Parsing of XCOFF executable (AIX) + +package objfile + +import ( + "debug/dwarf" + "fmt" + "loov.dev/lensm/internal/go/src/xcoff" + "io" + "unicode" +) + +type xcoffFile struct { + xcoff *xcoff.File +} + +func openXcoff(r io.ReaderAt) (rawFile, error) { + f, err := xcoff.NewFile(r) + if err != nil { + return nil, err + } + return &xcoffFile{f}, nil +} + +func (f *xcoffFile) symbols() ([]Sym, error) { + var syms []Sym + for _, s := range f.xcoff.Symbols { + const ( + N_UNDEF = 0 // An undefined (extern) symbol + N_ABS = -1 // An absolute symbol (e_value is a constant, not an address) + N_DEBUG = -2 // A debugging symbol + ) + sym := Sym{Name: s.Name, Addr: s.Value, Code: '?'} + + switch s.SectionNumber { + case N_UNDEF: + sym.Code = 'U' + case N_ABS: + sym.Code = 'C' + case N_DEBUG: + sym.Code = '?' + default: + if s.SectionNumber < 0 || len(f.xcoff.Sections) < s.SectionNumber { + return nil, fmt.Errorf("invalid section number in symbol table") + } + sect := f.xcoff.Sections[s.SectionNumber-1] + + // debug/xcoff returns an offset in the section not the actual address + sym.Addr += sect.VirtualAddress + + if s.AuxCSect.SymbolType&0x3 == xcoff.XTY_LD { + // The size of a function is contained in the + // AUX_FCN entry + sym.Size = s.AuxFcn.Size + } else { + sym.Size = s.AuxCSect.Length + } + + sym.Size = s.AuxCSect.Length + + switch sect.Type { + case xcoff.STYP_TEXT: + if s.AuxCSect.StorageMappingClass == xcoff.XMC_RO { + sym.Code = 'R' + } else { + sym.Code = 'T' + } + case xcoff.STYP_DATA: + sym.Code = 'D' + case xcoff.STYP_BSS: + sym.Code = 'B' + } + + if s.StorageClass == xcoff.C_HIDEXT { + // Local symbol + sym.Code = unicode.ToLower(sym.Code) + } + + } + syms = append(syms, sym) + } + + return syms, nil +} + +func (f *xcoffFile) pcln() (textStart uint64, pclntab []byte, err error) { + if sect := f.xcoff.Section(".text"); sect != nil { + textStart = sect.VirtualAddress + } + if pclntab, err = loadXCOFFTable(f.xcoff, "runtime.pclntab", "runtime.epclntab"); err != nil { + return 0, nil, err + } + return textStart, pclntab, nil +} + +func (f *xcoffFile) text() (textStart uint64, text []byte, err error) { + sect := f.xcoff.Section(".text") + if sect == nil { + return 0, nil, fmt.Errorf("text section not found") + } + textStart = sect.VirtualAddress + text, err = sect.Data() + return +} + +func findXCOFFSymbol(f *xcoff.File, name string) (*xcoff.Symbol, error) { + for _, s := range f.Symbols { + if s.Name != name { + continue + } + if s.SectionNumber <= 0 { + return nil, fmt.Errorf("symbol %s: invalid section number %d", name, s.SectionNumber) + } + if len(f.Sections) < s.SectionNumber { + return nil, fmt.Errorf("symbol %s: section number %d is larger than max %d", name, s.SectionNumber, len(f.Sections)) + } + return s, nil + } + return nil, fmt.Errorf("no %s symbol found", name) +} + +func loadXCOFFTable(f *xcoff.File, sname, ename string) ([]byte, error) { + ssym, err := findXCOFFSymbol(f, sname) + if err != nil { + return nil, err + } + esym, err := findXCOFFSymbol(f, ename) + if err != nil { + return nil, err + } + if ssym.SectionNumber != esym.SectionNumber { + return nil, fmt.Errorf("%s and %s symbols must be in the same section", sname, ename) + } + sect := f.Sections[ssym.SectionNumber-1] + data, err := sect.Data() + if err != nil { + return nil, err + } + return data[ssym.Value:esym.Value], nil +} + +func (f *xcoffFile) goarch() string { + switch f.xcoff.TargetMachine { + case xcoff.U802TOCMAGIC: + return "ppc" + case xcoff.U64_TOCMAGIC: + return "ppc64" + } + return "" +} + +func (f *xcoffFile) loadAddress() (uint64, error) { + return 0, fmt.Errorf("unknown load address") +} + +func (f *xcoffFile) dwarf() (*dwarf.Data, error) { + return f.xcoff.DWARF() +} diff --git a/internal/go/src/saferio/io.go b/internal/go/src/saferio/io.go new file mode 100755 index 0000000..5c428e6 --- /dev/null +++ b/internal/go/src/saferio/io.go @@ -0,0 +1,132 @@ +// Copyright 2022 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package saferio provides I/O functions that avoid allocating large +// amounts of memory unnecessarily. This is intended for packages that +// read data from an [io.Reader] where the size is part of the input +// data but the input may be corrupt, or may be provided by an +// untrustworthy attacker. +package saferio + +import ( + "io" + "unsafe" +) + +// chunk is an arbitrary limit on how much memory we are willing +// to allocate without concern. +const chunk = 10 << 20 // 10M + +// ReadData reads n bytes from the input stream, but avoids allocating +// all n bytes if n is large. This avoids crashing the program by +// allocating all n bytes in cases where n is incorrect. +// +// The error is io.EOF only if no bytes were read. +// If an io.EOF happens after reading some but not all the bytes, +// ReadData returns io.ErrUnexpectedEOF. +func ReadData(r io.Reader, n uint64) ([]byte, error) { + if int64(n) < 0 || n != uint64(int(n)) { + // n is too large to fit in int, so we can't allocate + // a buffer large enough. Treat this as a read failure. + return nil, io.ErrUnexpectedEOF + } + + if n < chunk { + buf := make([]byte, n) + _, err := io.ReadFull(r, buf) + if err != nil { + return nil, err + } + return buf, nil + } + + var buf []byte + buf1 := make([]byte, chunk) + for n > 0 { + next := n + if next > chunk { + next = chunk + } + _, err := io.ReadFull(r, buf1[:next]) + if err != nil { + if len(buf) > 0 && err == io.EOF { + err = io.ErrUnexpectedEOF + } + return nil, err + } + buf = append(buf, buf1[:next]...) + n -= next + } + return buf, nil +} + +// ReadDataAt reads n bytes from the input stream at off, but avoids +// allocating all n bytes if n is large. This avoids crashing the program +// by allocating all n bytes in cases where n is incorrect. +func ReadDataAt(r io.ReaderAt, n uint64, off int64) ([]byte, error) { + if int64(n) < 0 || n != uint64(int(n)) { + // n is too large to fit in int, so we can't allocate + // a buffer large enough. Treat this as a read failure. + return nil, io.ErrUnexpectedEOF + } + + if n < chunk { + buf := make([]byte, n) + _, err := r.ReadAt(buf, off) + if err != nil { + // io.SectionReader can return EOF for n == 0, + // but for our purposes that is a success. + if err != io.EOF || n > 0 { + return nil, err + } + } + return buf, nil + } + + var buf []byte + buf1 := make([]byte, chunk) + for n > 0 { + next := n + if next > chunk { + next = chunk + } + _, err := r.ReadAt(buf1[:next], off) + if err != nil { + return nil, err + } + buf = append(buf, buf1[:next]...) + n -= next + off += int64(next) + } + return buf, nil +} + +// SliceCapWithSize returns the capacity to use when allocating a slice. +// After the slice is allocated with the capacity, it should be +// built using append. This will avoid allocating too much memory +// if the capacity is large and incorrect. +// +// A negative result means that the value is always too big. +func SliceCapWithSize(size, c uint64) int { + if int64(c) < 0 || c != uint64(int(c)) { + return -1 + } + if size > 0 && c > (1<<64-1)/size { + return -1 + } + if c*size > chunk { + c = chunk / size + if c == 0 { + c = 1 + } + } + return int(c) +} + +// SliceCap is like SliceCapWithSize but using generics. +func SliceCap[E any](c uint64) int { + var v E + size := uint64(unsafe.Sizeof(v)) + return SliceCapWithSize(size, c) +} diff --git a/internal/go/src/src/pos.go b/internal/go/src/src/pos.go new file mode 100755 index 0000000..10609fc --- /dev/null +++ b/internal/go/src/src/pos.go @@ -0,0 +1,478 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file implements the encoding of source positions. + +package src + +import ( + "bytes" + "fmt" + "io" +) + +// A Pos encodes a source position consisting of a (line, column) number pair +// and a position base. A zero Pos is a ready to use "unknown" position (nil +// position base and zero line number). +// +// The (line, column) values refer to a position in a file independent of any +// position base ("absolute" file position). +// +// The position base is used to determine the "relative" position, that is the +// filename and line number relative to the position base. If the base refers +// to the current file, there is no difference between absolute and relative +// positions. If it refers to a //line directive, a relative position is relative +// to that directive. A position base in turn contains the position at which it +// was introduced in the current file. +type Pos struct { + base *PosBase + lico +} + +// NoPos is a valid unknown position. +var NoPos Pos + +// MakePos creates a new Pos value with the given base, and (file-absolute) +// line and column. +func MakePos(base *PosBase, line, col uint) Pos { + return Pos{base, makeLico(line, col)} +} + +// IsKnown reports whether the position p is known. +// A position is known if it either has a non-nil +// position base, or a non-zero line number. +func (p Pos) IsKnown() bool { + return p.base != nil || p.Line() != 0 +} + +// Before reports whether the position p comes before q in the source. +// For positions in different files, ordering is by filename. +func (p Pos) Before(q Pos) bool { + n, m := p.Filename(), q.Filename() + return n < m || n == m && p.lico < q.lico +} + +// After reports whether the position p comes after q in the source. +// For positions in different files, ordering is by filename. +func (p Pos) After(q Pos) bool { + n, m := p.Filename(), q.Filename() + return n > m || n == m && p.lico > q.lico +} + +func (p Pos) LineNumber() string { + if !p.IsKnown() { + return "?" + } + return p.lico.lineNumber() +} + +func (p Pos) LineNumberHTML() string { + if !p.IsKnown() { + return "?" + } + return p.lico.lineNumberHTML() +} + +// Filename returns the name of the actual file containing this position. +func (p Pos) Filename() string { return p.base.Pos().RelFilename() } + +// Base returns the position base. +func (p Pos) Base() *PosBase { return p.base } + +// SetBase sets the position base. +func (p *Pos) SetBase(base *PosBase) { p.base = base } + +// RelFilename returns the filename recorded with the position's base. +func (p Pos) RelFilename() string { return p.base.Filename() } + +// RelLine returns the line number relative to the position's base. +func (p Pos) RelLine() uint { + b := p.base + if b.Line() == 0 { + // base line is unknown => relative line is unknown + return 0 + } + return b.Line() + (p.Line() - b.Pos().Line()) +} + +// RelCol returns the column number relative to the position's base. +func (p Pos) RelCol() uint { + b := p.base + if b.Col() == 0 { + // base column is unknown => relative column is unknown + // (the current specification for line directives requires + // this to apply until the next PosBase/line directive, + // not just until the new newline) + return 0 + } + if p.Line() == b.Pos().Line() { + // p on same line as p's base => column is relative to p's base + return b.Col() + (p.Col() - b.Pos().Col()) + } + return p.Col() +} + +// AbsFilename() returns the absolute filename recorded with the position's base. +func (p Pos) AbsFilename() string { return p.base.AbsFilename() } + +// FileIndex returns the file index of the position's base's absolute +// filename within the PosTable that it was registered. +func (p Pos) FileIndex() int { return p.base.FileIndex() } + +func (p Pos) String() string { + return p.Format(true, true) +} + +// Format formats a position as "filename:line" or "filename:line:column", +// controlled by the showCol flag and if the column is known (!= 0). +// For positions relative to line directives, the original position is +// shown as well, as in "filename:line[origfile:origline:origcolumn]" if +// showOrig is set. +func (p Pos) Format(showCol, showOrig bool) string { + buf := new(bytes.Buffer) + p.WriteTo(buf, showCol, showOrig) + return buf.String() +} + +// WriteTo a position to w, formatted as Format does. +func (p Pos) WriteTo(w io.Writer, showCol, showOrig bool) { + if !p.IsKnown() { + io.WriteString(w, "") + return + } + + if b := p.base; b == b.Pos().base { + // base is file base (incl. nil) + format(w, p.Filename(), p.Line(), p.Col(), showCol) + return + } + + // base is relative + // Print the column only for the original position since the + // relative position's column information may be bogus (it's + // typically generated code and we can't say much about the + // original source at that point but for the file:line info + // that's provided via a line directive). + // TODO(gri) This may not be true if we have an inlining base. + // We may want to differentiate at some point. + format(w, p.RelFilename(), p.RelLine(), p.RelCol(), showCol) + if showOrig { + io.WriteString(w, "[") + format(w, p.Filename(), p.Line(), p.Col(), showCol) + io.WriteString(w, "]") + } +} + +// format formats a (filename, line, col) tuple as "filename:line" (showCol +// is false or col == 0) or "filename:line:column" (showCol is true and col != 0). +func format(w io.Writer, filename string, line, col uint, showCol bool) { + io.WriteString(w, filename) + io.WriteString(w, ":") + fmt.Fprint(w, line) + // col == 0 and col == colMax are interpreted as unknown column values + if showCol && 0 < col && col < colMax { + io.WriteString(w, ":") + fmt.Fprint(w, col) + } +} + +// formatstr wraps format to return a string. +func formatstr(filename string, line, col uint, showCol bool) string { + buf := new(bytes.Buffer) + format(buf, filename, line, col, showCol) + return buf.String() +} + +// ---------------------------------------------------------------------------- +// PosBase + +// A PosBase encodes a filename and base position. +// Typically, each file and line directive introduce a PosBase. +type PosBase struct { + pos Pos // position at which the relative position is (line, col) + filename string // file name used to open source file, for error messages + absFilename string // absolute file name, for PC-Line tables + line, col uint // relative line, column number at pos + inl int // inlining index (see cmd/internal/obj/inl.go) + fileIndex int // index of absFilename within PosTable.FileTable +} + +// NewFileBase returns a new *PosBase for a file with the given (relative and +// absolute) filenames. +func NewFileBase(filename, absFilename string) *PosBase { + base := &PosBase{ + filename: filename, + absFilename: absFilename, + line: 1, + col: 1, + inl: -1, + fileIndex: -1, + } + base.pos = MakePos(base, 1, 1) + return base +} + +// NewLinePragmaBase returns a new *PosBase for a line directive of the form +// +// //line filename:line:col +// /*line filename:line:col*/ +// +// at position pos. +func NewLinePragmaBase(pos Pos, filename, absFilename string, line, col uint) *PosBase { + return &PosBase{pos, filename, absFilename, line, col, -1, -1} +} + +// NewInliningBase returns a copy of the orig PosBase with the given inlining +// index. If orig == nil, NewInliningBase panics. +func NewInliningBase(orig *PosBase, inlTreeIndex int) *PosBase { + if orig == nil { + panic("no old PosBase") + } + base := *orig + base.inl = inlTreeIndex + base.fileIndex = -1 + if orig == orig.pos.base { + base.pos.base = &base + } + return &base +} + +var noPos Pos + +// Pos returns the position at which base is located. +// If b == nil, the result is the zero position. +func (b *PosBase) Pos() *Pos { + if b != nil { + return &b.pos + } + return &noPos +} + +// Filename returns the filename recorded with the base. +// If b == nil, the result is the empty string. +func (b *PosBase) Filename() string { + if b != nil { + return b.filename + } + return "" +} + +// AbsFilename returns the absolute filename recorded with the base. +// If b == nil, the result is the empty string. +func (b *PosBase) AbsFilename() string { + if b != nil { + return b.absFilename + } + return "" +} + +// FileSymPrefix is the linker symbol prefix that used to be used for +// linker pseudo-symbols representing file names. +const FileSymPrefix = "gofile.." + +// FileIndex returns the index of the base's absolute filename within +// its PosTable's FileTable. It panics if it hasn't been registered +// with a PosTable. If b == nil, the result is -1. +func (b *PosBase) FileIndex() int { + if b != nil { + if b.fileIndex < 0 { + panic("PosBase has no file index") + } + return b.fileIndex + } + return -1 +} + +// Line returns the line number recorded with the base. +// If b == nil, the result is 0. +func (b *PosBase) Line() uint { + if b != nil { + return b.line + } + return 0 +} + +// Col returns the column number recorded with the base. +// If b == nil, the result is 0. +func (b *PosBase) Col() uint { + if b != nil { + return b.col + } + return 0 +} + +// InliningIndex returns the index into the global inlining +// tree recorded with the base. If b == nil or the base has +// not been inlined, the result is < 0. +func (b *PosBase) InliningIndex() int { + if b != nil { + return b.inl + } + return -1 +} + +// ---------------------------------------------------------------------------- +// lico + +// A lico is a compact encoding of a LIne and COlumn number. +type lico uint32 + +// Layout constants: 20 bits for line, 8 bits for column, 2 for isStmt, 2 for pro/epilogue +// (If this is too tight, we can either make lico 64b wide, +// or we can introduce a tiered encoding where we remove column +// information as line numbers grow bigger; similar to what gcc +// does.) +// The bitfield order is chosen to make IsStmt be the least significant +// part of a position; its use is to communicate statement edges through +// instruction scrambling in code generation, not to impose an order. +// TODO: Prologue and epilogue are perhaps better handled as pseudo-ops for the assembler, +// because they have almost no interaction with other uses of the position. +const ( + lineBits, lineMax = 20, 1<= lineMax { + // cannot represent line, use max. line so we have some information + line = lineMax + // Drop column information if line number saturates. + // Ensures line+col is monotonic. See issue 51193. + col = 0 + } + if col > colMax { + // cannot represent column, use max. column so we have some information + col = colMax + } + // default is not-sure-if-statement + return makeLicoRaw(line, col) +} + +func (x lico) Line() uint { return uint(x) >> lineShift } +func (x lico) SameLine(y lico) bool { return 0 == (x^y)&^lico(1<> colShift & colMax } +func (x lico) IsStmt() uint { + if x == 0 { + return PosNotStmt + } + return uint(x) >> isStmtShift & isStmtMax +} +func (x lico) Xlogue() PosXlogue { + return PosXlogue(uint(x) >> xlogueShift & xlogueMax) +} + +// withNotStmt returns a lico for the same location, but not a statement +func (x lico) withNotStmt() lico { + return x.withStmt(PosNotStmt) +} + +// withDefaultStmt returns a lico for the same location, with default isStmt +func (x lico) withDefaultStmt() lico { + return x.withStmt(PosDefaultStmt) +} + +// withIsStmt returns a lico for the same location, tagged as definitely a statement +func (x lico) withIsStmt() lico { + return x.withStmt(PosIsStmt) +} + +// withXlogue attaches a prologue/epilogue attribute to a lico +func (x lico) withXlogue(xlogue PosXlogue) lico { + if x == 0 { + if xlogue == 0 { + return x + } + // Normalize 0 to "not a statement" + x = lico(PosNotStmt << isStmtShift) + } + return lico(uint(x) & ^uint(xlogueMax<%s%d", style, pfx, x.Line(), style) +} + +func (x lico) atColumn1() lico { + return makeLico(x.Line(), 1).withIsStmt() +} diff --git a/internal/go/src/src/xpos.go b/internal/go/src/src/xpos.go new file mode 100755 index 0000000..a745059 --- /dev/null +++ b/internal/go/src/src/xpos.go @@ -0,0 +1,183 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file implements the compressed encoding of source +// positions using a lookup table. + +package src + +// XPos is a more compact representation of Pos. +type XPos struct { + index int32 + lico +} + +// NoXPos is a valid unknown position. +var NoXPos XPos + +// IsKnown reports whether the position p is known. +// XPos.IsKnown() matches Pos.IsKnown() for corresponding +// positions. +func (p XPos) IsKnown() bool { + return p.index != 0 || p.Line() != 0 +} + +// Before reports whether the position p comes before q in the source. +// For positions with different bases, ordering is by base index. +func (p XPos) Before(q XPos) bool { + n, m := p.index, q.index + return n < m || n == m && p.lico < q.lico +} + +// SameFile reports whether p and q are positions in the same file. +func (p XPos) SameFile(q XPos) bool { + return p.index == q.index +} + +// SameFileAndLine reports whether p and q are positions on the same line in the same file. +func (p XPos) SameFileAndLine(q XPos) bool { + return p.index == q.index && p.lico.SameLine(q.lico) +} + +// After reports whether the position p comes after q in the source. +// For positions with different bases, ordering is by base index. +func (p XPos) After(q XPos) bool { + n, m := p.index, q.index + return n > m || n == m && p.lico > q.lico +} + +// WithNotStmt returns the same location to be marked with DWARF is_stmt=0 +func (p XPos) WithNotStmt() XPos { + p.lico = p.lico.withNotStmt() + return p +} + +// WithDefaultStmt returns the same location with undetermined is_stmt +func (p XPos) WithDefaultStmt() XPos { + p.lico = p.lico.withDefaultStmt() + return p +} + +// WithIsStmt returns the same location to be marked with DWARF is_stmt=1 +func (p XPos) WithIsStmt() XPos { + p.lico = p.lico.withIsStmt() + return p +} + +// WithBogusLine returns a bogus line that won't match any recorded for the source code. +// Its use is to disrupt the statements within an infinite loop so that the debugger +// will not itself loop infinitely waiting for the line number to change. +// gdb chooses not to display the bogus line; delve shows it with a complaint, but the +// alternative behavior is to hang. +func (p XPos) WithBogusLine() XPos { + if p.index == 0 { + // See #35652 + panic("Assigning a bogus line to XPos with no file will cause mysterious downstream failures.") + } + p.lico = makeBogusLico() + return p +} + +// WithXlogue returns the same location but marked with DWARF function prologue/epilogue +func (p XPos) WithXlogue(x PosXlogue) XPos { + p.lico = p.lico.withXlogue(x) + return p +} + +// LineNumber returns a string for the line number, "?" if it is not known. +func (p XPos) LineNumber() string { + if !p.IsKnown() { + return "?" + } + return p.lico.lineNumber() +} + +// FileIndex returns a smallish non-negative integer corresponding to the +// file for this source position. Smallish is relative; it can be thousands +// large, but not millions. +func (p XPos) FileIndex() int32 { + return p.index +} + +func (p XPos) LineNumberHTML() string { + if !p.IsKnown() { + return "?" + } + return p.lico.lineNumberHTML() +} + +// AtColumn1 returns the same location but shifted to column 1. +func (p XPos) AtColumn1() XPos { + p.lico = p.lico.atColumn1() + return p +} + +// A PosTable tracks Pos -> XPos conversions and vice versa. +// Its zero value is a ready-to-use PosTable. +type PosTable struct { + baseList []*PosBase + indexMap map[*PosBase]int + nameMap map[string]int // Maps file symbol name to index for debug information. +} + +// XPos returns the corresponding XPos for the given pos, +// adding pos to t if necessary. +func (t *PosTable) XPos(pos Pos) XPos { + return XPos{t.baseIndex(pos.base), pos.lico} +} + +func (t *PosTable) baseIndex(base *PosBase) int32 { + if base == nil { + return 0 + } + + if i, ok := t.indexMap[base]; ok { + return int32(i) + } + + if base.fileIndex >= 0 { + panic("PosBase already registered with a PosTable") + } + + if t.indexMap == nil { + t.baseList = append(t.baseList, nil) + t.indexMap = make(map[*PosBase]int) + t.nameMap = make(map[string]int) + } + + i := len(t.baseList) + t.indexMap[base] = i + t.baseList = append(t.baseList, base) + + fileIndex, ok := t.nameMap[base.absFilename] + if !ok { + fileIndex = len(t.nameMap) + t.nameMap[base.absFilename] = fileIndex + } + base.fileIndex = fileIndex + + return int32(i) +} + +// Pos returns the corresponding Pos for the given p. +// If p cannot be translated via t, the function panics. +func (t *PosTable) Pos(p XPos) Pos { + var base *PosBase + if p.index != 0 { + base = t.baseList[p.index] + } + return Pos{base, p.lico} +} + +// FileTable returns a slice of all files used to build this package. +func (t *PosTable) FileTable() []string { + // Create a LUT of the global package level file indices. This table is what + // is written in the debug_lines header, the file[N] will be referenced as + // N+1 in the debug_lines table. + fileLUT := make([]string, len(t.nameMap)) + for str, i := range t.nameMap { + fileLUT[i] = str + } + return fileLUT +} diff --git a/internal/go/src/sys/arch.go b/internal/go/src/sys/arch.go new file mode 100755 index 0000000..e0f0edd --- /dev/null +++ b/internal/go/src/sys/arch.go @@ -0,0 +1,287 @@ +// Copyright 2016 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sys + +import ( + "encoding/binary" + "loov.dev/lensm/internal/go/src/goarch" +) + +// TODO: just use goarch.ArchFamilyType directly +type ArchFamily = goarch.ArchFamilyType + +const ( + AMD64 = goarch.AMD64 + ARM = goarch.ARM + ARM64 = goarch.ARM64 + I386 = goarch.I386 + Loong64 = goarch.LOONG64 + MIPS = goarch.MIPS + MIPS64 = goarch.MIPS64 + PPC64 = goarch.PPC64 + RISCV64 = goarch.RISCV64 + S390X = goarch.S390X + Wasm = goarch.WASM +) + +// Arch represents an individual architecture. +type Arch struct { + Name string + Family ArchFamily + + ByteOrder binary.ByteOrder + + // PtrSize is the size in bytes of pointers and the + // predeclared "int", "uint", and "uintptr" types. + PtrSize int + + // RegSize is the size in bytes of general purpose registers. + RegSize int + + // MinLC is the minimum length of an instruction code. + MinLC int + + // Alignment is maximum alignment required by the architecture + // for any (compiler-generated) load or store instruction. + // Loads or stores smaller than Alignment must be naturally aligned. + // Loads or stores larger than Alignment need only be Alignment-aligned. + Alignment int8 + + // CanMergeLoads reports whether the backend optimization passes + // can combine adjacent loads into a single larger, possibly unaligned, load. + // Note that currently the optimizations must be able to handle little endian byte order. + CanMergeLoads bool + + // CanJumpTable reports whether the backend can handle + // compiling a jump table. + CanJumpTable bool + + // HasLR indicates that this architecture uses a link register + // for calls. + HasLR bool + + // FixedFrameSize is the smallest possible offset from the + // hardware stack pointer to a local variable on the stack. + // Architectures that use a link register save its value on + // the stack in the function prologue and so always have a + // pointer between the hardware stack pointer and the local + // variable area. + FixedFrameSize int64 +} + +// InFamily reports whether a is a member of any of the specified +// architecture families. +func (a *Arch) InFamily(xs ...ArchFamily) bool { + for _, x := range xs { + if a.Family == x { + return true + } + } + return false +} + +var Arch386 = &Arch{ + Name: "386", + Family: I386, + ByteOrder: binary.LittleEndian, + PtrSize: 4, + RegSize: 4, + MinLC: 1, + Alignment: 1, + CanMergeLoads: true, + HasLR: false, + FixedFrameSize: 0, +} + +var ArchAMD64 = &Arch{ + Name: "amd64", + Family: AMD64, + ByteOrder: binary.LittleEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 1, + Alignment: 1, + CanMergeLoads: true, + CanJumpTable: true, + HasLR: false, + FixedFrameSize: 0, +} + +var ArchARM = &Arch{ + Name: "arm", + Family: ARM, + ByteOrder: binary.LittleEndian, + PtrSize: 4, + RegSize: 4, + MinLC: 4, + Alignment: 4, // TODO: just for arm5? + CanMergeLoads: false, + HasLR: true, + FixedFrameSize: 4, // LR +} + +var ArchARM64 = &Arch{ + Name: "arm64", + Family: ARM64, + ByteOrder: binary.LittleEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 4, + Alignment: 1, + CanMergeLoads: true, + CanJumpTable: true, + HasLR: true, + FixedFrameSize: 8, // LR +} + +var ArchLoong64 = &Arch{ + Name: "loong64", + Family: Loong64, + ByteOrder: binary.LittleEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 4, + Alignment: 8, // Unaligned accesses are not guaranteed to be fast + CanMergeLoads: true, + CanJumpTable: true, + HasLR: true, + FixedFrameSize: 8, // LR +} + +var ArchMIPS = &Arch{ + Name: "mips", + Family: MIPS, + ByteOrder: binary.BigEndian, + PtrSize: 4, + RegSize: 4, + MinLC: 4, + Alignment: 4, + CanMergeLoads: false, + HasLR: true, + FixedFrameSize: 4, // LR +} + +var ArchMIPSLE = &Arch{ + Name: "mipsle", + Family: MIPS, + ByteOrder: binary.LittleEndian, + PtrSize: 4, + RegSize: 4, + MinLC: 4, + Alignment: 4, + CanMergeLoads: false, + HasLR: true, + FixedFrameSize: 4, // LR +} + +var ArchMIPS64 = &Arch{ + Name: "mips64", + Family: MIPS64, + ByteOrder: binary.BigEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 4, + Alignment: 8, + CanMergeLoads: false, + HasLR: true, + FixedFrameSize: 8, // LR +} + +var ArchMIPS64LE = &Arch{ + Name: "mips64le", + Family: MIPS64, + ByteOrder: binary.LittleEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 4, + Alignment: 8, + CanMergeLoads: false, + HasLR: true, + FixedFrameSize: 8, // LR +} + +var ArchPPC64 = &Arch{ + Name: "ppc64", + Family: PPC64, + ByteOrder: binary.BigEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 4, + Alignment: 1, + CanMergeLoads: true, + HasLR: true, + // PIC code on ppc64le requires 32 bytes of stack, and it's + // easier to just use that much stack always. + FixedFrameSize: 4 * 8, +} + +var ArchPPC64LE = &Arch{ + Name: "ppc64le", + Family: PPC64, + ByteOrder: binary.LittleEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 4, + Alignment: 1, + CanMergeLoads: true, + HasLR: true, + FixedFrameSize: 4 * 8, +} + +var ArchRISCV64 = &Arch{ + Name: "riscv64", + Family: RISCV64, + ByteOrder: binary.LittleEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 2, + Alignment: 8, // riscv unaligned loads work, but are really slow (trap + simulated by OS) + CanMergeLoads: false, + HasLR: true, + FixedFrameSize: 8, // LR +} + +var ArchS390X = &Arch{ + Name: "s390x", + Family: S390X, + ByteOrder: binary.BigEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 2, + Alignment: 1, + CanMergeLoads: true, + HasLR: true, + FixedFrameSize: 8, // LR +} + +var ArchWasm = &Arch{ + Name: "wasm", + Family: Wasm, + ByteOrder: binary.LittleEndian, + PtrSize: 8, + RegSize: 8, + MinLC: 1, + Alignment: 1, + CanMergeLoads: false, + HasLR: false, + FixedFrameSize: 0, +} + +var Archs = [...]*Arch{ + Arch386, + ArchAMD64, + ArchARM, + ArchARM64, + ArchLoong64, + ArchMIPS, + ArchMIPSLE, + ArchMIPS64, + ArchMIPS64LE, + ArchPPC64, + ArchPPC64LE, + ArchRISCV64, + ArchS390X, + ArchWasm, +} diff --git a/internal/go/src/sys/args.go b/internal/go/src/sys/args.go new file mode 100755 index 0000000..cc9fb64 --- /dev/null +++ b/internal/go/src/sys/args.go @@ -0,0 +1,13 @@ +// Copyright 2021 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package sys + +// ExecArgLengthLimit is the number of bytes we can safely +// pass as arguments to an exec.Command. +// +// Windows has a limit of 32 KB. To be conservative and not worry about whether +// that includes spaces or not, just use 30 KB. Darwin's limit is less clear. +// The OS claims 256KB, but we've seen failures with arglen as small as 50KB. +const ExecArgLengthLimit = (30 << 10) diff --git a/internal/go/src/unsafeheader/unsafeheader.go b/internal/go/src/unsafeheader/unsafeheader.go new file mode 100755 index 0000000..6d092c6 --- /dev/null +++ b/internal/go/src/unsafeheader/unsafeheader.go @@ -0,0 +1,37 @@ +// Copyright 2020 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package unsafeheader contains header declarations for the Go runtime's slice +// and string implementations. +// +// This package allows packages that cannot import "reflect" to use types that +// are tested to be equivalent to reflect.SliceHeader and reflect.StringHeader. +package unsafeheader + +import ( + "unsafe" +) + +// Slice is the runtime representation of a slice. +// It cannot be used safely or portably and its representation may +// change in a later release. +// +// Unlike reflect.SliceHeader, its Data field is sufficient to guarantee the +// data it references will not be garbage collected. +type Slice struct { + Data unsafe.Pointer + Len int + Cap int +} + +// String is the runtime representation of a string. +// It cannot be used safely or portably and its representation may +// change in a later release. +// +// Unlike reflect.StringHeader, its Data field is sufficient to guarantee the +// data it references will not be garbage collected. +type String struct { + Data unsafe.Pointer + Len int +} diff --git a/internal/go/src/xcoff/ar.go b/internal/go/src/xcoff/ar.go new file mode 100755 index 0000000..e616f37 --- /dev/null +++ b/internal/go/src/xcoff/ar.go @@ -0,0 +1,226 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcoff + +import ( + "encoding/binary" + "fmt" + "io" + "os" + "strconv" + "strings" +) + +const ( + SAIAMAG = 0x8 + AIAFMAG = "`\n" + AIAMAG = "\n" + AIAMAGBIG = "\n" + + // Sizeof + FL_HSZ_BIG = 0x80 + AR_HSZ_BIG = 0x70 +) + +type bigarFileHeader struct { + Flmagic [SAIAMAG]byte // Archive magic string + Flmemoff [20]byte // Member table offset + Flgstoff [20]byte // 32-bits global symtab offset + Flgst64off [20]byte // 64-bits global symtab offset + Flfstmoff [20]byte // First member offset + Fllstmoff [20]byte // Last member offset + Flfreeoff [20]byte // First member on free list offset +} + +type bigarMemberHeader struct { + Arsize [20]byte // File member size + Arnxtmem [20]byte // Next member pointer + Arprvmem [20]byte // Previous member pointer + Ardate [12]byte // File member date + Aruid [12]byte // File member uid + Argid [12]byte // File member gid + Armode [12]byte // File member mode (octal) + Arnamlen [4]byte // File member name length + // _ar_nam is removed because it's easier to get name without it. +} + +// Archive represents an open AIX big archive. +type Archive struct { + ArchiveHeader + Members []*Member + + closer io.Closer +} + +// ArchiveHeader holds information about a big archive file header +type ArchiveHeader struct { + magic string +} + +// Member represents a member of an AIX big archive. +type Member struct { + MemberHeader + sr *io.SectionReader +} + +// MemberHeader holds information about a big archive member +type MemberHeader struct { + Name string + Size uint64 +} + +// OpenArchive opens the named archive using os.Open and prepares it for use +// as an AIX big archive. +func OpenArchive(name string) (*Archive, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + arch, err := NewArchive(f) + if err != nil { + f.Close() + return nil, err + } + arch.closer = f + return arch, nil +} + +// Close closes the Archive. +// If the Archive was created using NewArchive directly instead of OpenArchive, +// Close has no effect. +func (a *Archive) Close() error { + var err error + if a.closer != nil { + err = a.closer.Close() + a.closer = nil + } + return err +} + +// NewArchive creates a new Archive for accessing an AIX big archive in an underlying reader. +func NewArchive(r io.ReaderAt) (*Archive, error) { + parseDecimalBytes := func(b []byte) (int64, error) { + return strconv.ParseInt(strings.TrimSpace(string(b)), 10, 64) + } + sr := io.NewSectionReader(r, 0, 1<<63-1) + + // Read File Header + var magic [SAIAMAG]byte + if _, err := sr.ReadAt(magic[:], 0); err != nil { + return nil, err + } + + arch := new(Archive) + switch string(magic[:]) { + case AIAMAGBIG: + arch.magic = string(magic[:]) + case AIAMAG: + return nil, fmt.Errorf("small AIX archive not supported") + default: + return nil, fmt.Errorf("unrecognised archive magic: 0x%x", magic) + } + + var fhdr bigarFileHeader + if _, err := sr.Seek(0, io.SeekStart); err != nil { + return nil, err + } + if err := binary.Read(sr, binary.BigEndian, &fhdr); err != nil { + return nil, err + } + + off, err := parseDecimalBytes(fhdr.Flfstmoff[:]) + if err != nil { + return nil, fmt.Errorf("error parsing offset of first member in archive header(%q); %v", fhdr, err) + } + + if off == 0 { + // Occurs if the archive is empty. + return arch, nil + } + + lastoff, err := parseDecimalBytes(fhdr.Fllstmoff[:]) + if err != nil { + return nil, fmt.Errorf("error parsing offset of first member in archive header(%q); %v", fhdr, err) + } + + // Read members + for { + // Read Member Header + // The member header is normally 2 bytes larger. But it's easier + // to read the name if the header is read without _ar_nam. + // However, AIAFMAG must be read afterward. + if _, err := sr.Seek(off, io.SeekStart); err != nil { + return nil, err + } + + var mhdr bigarMemberHeader + if err := binary.Read(sr, binary.BigEndian, &mhdr); err != nil { + return nil, err + } + + member := new(Member) + arch.Members = append(arch.Members, member) + + size, err := parseDecimalBytes(mhdr.Arsize[:]) + if err != nil { + return nil, fmt.Errorf("error parsing size in member header(%q); %v", mhdr, err) + } + member.Size = uint64(size) + + // Read name + namlen, err := parseDecimalBytes(mhdr.Arnamlen[:]) + if err != nil { + return nil, fmt.Errorf("error parsing name length in member header(%q); %v", mhdr, err) + } + name := make([]byte, namlen) + if err := binary.Read(sr, binary.BigEndian, name); err != nil { + return nil, err + } + member.Name = string(name) + + fileoff := off + AR_HSZ_BIG + namlen + if fileoff&1 != 0 { + fileoff++ + if _, err := sr.Seek(1, io.SeekCurrent); err != nil { + return nil, err + } + } + + // Read AIAFMAG string + var fmag [2]byte + if err := binary.Read(sr, binary.BigEndian, &fmag); err != nil { + return nil, err + } + if string(fmag[:]) != AIAFMAG { + return nil, fmt.Errorf("AIAFMAG not found after member header") + } + + fileoff += 2 // Add the two bytes of AIAFMAG + member.sr = io.NewSectionReader(sr, fileoff, size) + + if off == lastoff { + break + } + off, err = parseDecimalBytes(mhdr.Arnxtmem[:]) + if err != nil { + return nil, fmt.Errorf("error parsing offset of first member in archive header(%q); %v", fhdr, err) + } + + } + + return arch, nil +} + +// GetFile returns the XCOFF file defined by member name. +// FIXME: This doesn't work if an archive has two members with the same +// name which can occur if an archive has both 32-bits and 64-bits files. +func (arch *Archive) GetFile(name string) (*File, error) { + for _, mem := range arch.Members { + if mem.Name == name { + return NewFile(mem.sr) + } + } + return nil, fmt.Errorf("unknown member %s in archive", name) +} diff --git a/internal/go/src/xcoff/file.go b/internal/go/src/xcoff/file.go new file mode 100755 index 0000000..3327ab7 --- /dev/null +++ b/internal/go/src/xcoff/file.go @@ -0,0 +1,693 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package xcoff implements access to XCOFF (Extended Common Object File Format) files. +package xcoff + +import ( + "debug/dwarf" + "encoding/binary" + "errors" + "fmt" + "loov.dev/lensm/internal/go/src/saferio" + "io" + "os" + "strings" +) + +// SectionHeader holds information about an XCOFF section header. +type SectionHeader struct { + Name string + VirtualAddress uint64 + Size uint64 + Type uint32 + Relptr uint64 + Nreloc uint32 +} + +type Section struct { + SectionHeader + Relocs []Reloc + io.ReaderAt + sr *io.SectionReader +} + +// AuxiliaryCSect holds information about an XCOFF symbol in an AUX_CSECT entry. +type AuxiliaryCSect struct { + Length int64 + StorageMappingClass int + SymbolType int +} + +// AuxiliaryFcn holds information about an XCOFF symbol in an AUX_FCN entry. +type AuxiliaryFcn struct { + Size int64 +} + +type Symbol struct { + Name string + Value uint64 + SectionNumber int + StorageClass int + AuxFcn AuxiliaryFcn + AuxCSect AuxiliaryCSect +} + +type Reloc struct { + VirtualAddress uint64 + Symbol *Symbol + Signed bool + InstructionFixed bool + Length uint8 + Type uint8 +} + +// ImportedSymbol holds information about an imported XCOFF symbol. +type ImportedSymbol struct { + Name string + Library string +} + +// FileHeader holds information about an XCOFF file header. +type FileHeader struct { + TargetMachine uint16 +} + +// A File represents an open XCOFF file. +type File struct { + FileHeader + Sections []*Section + Symbols []*Symbol + StringTable []byte + LibraryPaths []string + + closer io.Closer +} + +// Open opens the named file using os.Open and prepares it for use as an XCOFF binary. +func Open(name string) (*File, error) { + f, err := os.Open(name) + if err != nil { + return nil, err + } + ff, err := NewFile(f) + if err != nil { + f.Close() + return nil, err + } + ff.closer = f + return ff, nil +} + +// Close closes the File. +// If the File was created using NewFile directly instead of Open, +// Close has no effect. +func (f *File) Close() error { + var err error + if f.closer != nil { + err = f.closer.Close() + f.closer = nil + } + return err +} + +// Section returns the first section with the given name, or nil if no such +// section exists. +// Xcoff have section's name limited to 8 bytes. Some sections like .gosymtab +// can be trunked but this method will still find them. +func (f *File) Section(name string) *Section { + for _, s := range f.Sections { + if s.Name == name || (len(name) > 8 && s.Name == name[:8]) { + return s + } + } + return nil +} + +// SectionByType returns the first section in f with the +// given type, or nil if there is no such section. +func (f *File) SectionByType(typ uint32) *Section { + for _, s := range f.Sections { + if s.Type == typ { + return s + } + } + return nil +} + +// cstring converts ASCII byte sequence b to string. +// It stops once it finds 0 or reaches end of b. +func cstring(b []byte) string { + var i int + for i = 0; i < len(b) && b[i] != 0; i++ { + } + return string(b[:i]) +} + +// getString extracts a string from an XCOFF string table. +func getString(st []byte, offset uint32) (string, bool) { + if offset < 4 || int(offset) >= len(st) { + return "", false + } + return cstring(st[offset:]), true +} + +// NewFile creates a new File for accessing an XCOFF binary in an underlying reader. +func NewFile(r io.ReaderAt) (*File, error) { + sr := io.NewSectionReader(r, 0, 1<<63-1) + // Read XCOFF target machine + var magic uint16 + if err := binary.Read(sr, binary.BigEndian, &magic); err != nil { + return nil, err + } + if magic != U802TOCMAGIC && magic != U64_TOCMAGIC { + return nil, fmt.Errorf("unrecognised XCOFF magic: 0x%x", magic) + } + + f := new(File) + f.TargetMachine = magic + + // Read XCOFF file header + if _, err := sr.Seek(0, io.SeekStart); err != nil { + return nil, err + } + var nscns uint16 + var symptr uint64 + var nsyms uint32 + var opthdr uint16 + var hdrsz int + switch f.TargetMachine { + case U802TOCMAGIC: + fhdr := new(FileHeader32) + if err := binary.Read(sr, binary.BigEndian, fhdr); err != nil { + return nil, err + } + nscns = fhdr.Fnscns + symptr = uint64(fhdr.Fsymptr) + nsyms = fhdr.Fnsyms + opthdr = fhdr.Fopthdr + hdrsz = FILHSZ_32 + case U64_TOCMAGIC: + fhdr := new(FileHeader64) + if err := binary.Read(sr, binary.BigEndian, fhdr); err != nil { + return nil, err + } + nscns = fhdr.Fnscns + symptr = fhdr.Fsymptr + nsyms = fhdr.Fnsyms + opthdr = fhdr.Fopthdr + hdrsz = FILHSZ_64 + } + + if symptr == 0 || nsyms <= 0 { + return nil, fmt.Errorf("no symbol table") + } + + // Read string table (located right after symbol table). + offset := symptr + uint64(nsyms)*SYMESZ + if _, err := sr.Seek(int64(offset), io.SeekStart); err != nil { + return nil, err + } + // The first 4 bytes contain the length (in bytes). + var l uint32 + if err := binary.Read(sr, binary.BigEndian, &l); err != nil { + return nil, err + } + if l > 4 { + st, err := saferio.ReadDataAt(sr, uint64(l), int64(offset)) + if err != nil { + return nil, err + } + f.StringTable = st + } + + // Read section headers + if _, err := sr.Seek(int64(hdrsz)+int64(opthdr), io.SeekStart); err != nil { + return nil, err + } + c := saferio.SliceCap[*Section](uint64(nscns)) + if c < 0 { + return nil, fmt.Errorf("too many XCOFF sections (%d)", nscns) + } + f.Sections = make([]*Section, 0, c) + for i := 0; i < int(nscns); i++ { + var scnptr uint64 + s := new(Section) + switch f.TargetMachine { + case U802TOCMAGIC: + shdr := new(SectionHeader32) + if err := binary.Read(sr, binary.BigEndian, shdr); err != nil { + return nil, err + } + s.Name = cstring(shdr.Sname[:]) + s.VirtualAddress = uint64(shdr.Svaddr) + s.Size = uint64(shdr.Ssize) + scnptr = uint64(shdr.Sscnptr) + s.Type = shdr.Sflags + s.Relptr = uint64(shdr.Srelptr) + s.Nreloc = uint32(shdr.Snreloc) + case U64_TOCMAGIC: + shdr := new(SectionHeader64) + if err := binary.Read(sr, binary.BigEndian, shdr); err != nil { + return nil, err + } + s.Name = cstring(shdr.Sname[:]) + s.VirtualAddress = shdr.Svaddr + s.Size = shdr.Ssize + scnptr = shdr.Sscnptr + s.Type = shdr.Sflags + s.Relptr = shdr.Srelptr + s.Nreloc = shdr.Snreloc + } + r2 := r + if scnptr == 0 { // .bss must have all 0s + r2 = &nobitsSectionReader{} + } + s.sr = io.NewSectionReader(r2, int64(scnptr), int64(s.Size)) + s.ReaderAt = s.sr + f.Sections = append(f.Sections, s) + } + + // Symbol map needed by relocation + var idxToSym = make(map[int]*Symbol) + + // Read symbol table + if _, err := sr.Seek(int64(symptr), io.SeekStart); err != nil { + return nil, err + } + f.Symbols = make([]*Symbol, 0) + for i := 0; i < int(nsyms); i++ { + var numaux int + var ok, needAuxFcn bool + sym := new(Symbol) + switch f.TargetMachine { + case U802TOCMAGIC: + se := new(SymEnt32) + if err := binary.Read(sr, binary.BigEndian, se); err != nil { + return nil, err + } + numaux = int(se.Nnumaux) + sym.SectionNumber = int(se.Nscnum) + sym.StorageClass = int(se.Nsclass) + sym.Value = uint64(se.Nvalue) + needAuxFcn = se.Ntype&SYM_TYPE_FUNC != 0 && numaux > 1 + zeroes := binary.BigEndian.Uint32(se.Nname[:4]) + if zeroes != 0 { + sym.Name = cstring(se.Nname[:]) + } else { + offset := binary.BigEndian.Uint32(se.Nname[4:]) + sym.Name, ok = getString(f.StringTable, offset) + if !ok { + goto skip + } + } + case U64_TOCMAGIC: + se := new(SymEnt64) + if err := binary.Read(sr, binary.BigEndian, se); err != nil { + return nil, err + } + numaux = int(se.Nnumaux) + sym.SectionNumber = int(se.Nscnum) + sym.StorageClass = int(se.Nsclass) + sym.Value = se.Nvalue + needAuxFcn = se.Ntype&SYM_TYPE_FUNC != 0 && numaux > 1 + sym.Name, ok = getString(f.StringTable, se.Noffset) + if !ok { + goto skip + } + } + if sym.StorageClass != C_EXT && sym.StorageClass != C_WEAKEXT && sym.StorageClass != C_HIDEXT { + goto skip + } + // Must have at least one csect auxiliary entry. + if numaux < 1 || i+numaux >= int(nsyms) { + goto skip + } + + if sym.SectionNumber > int(nscns) { + goto skip + } + if sym.SectionNumber == 0 { + sym.Value = 0 + } else { + sym.Value -= f.Sections[sym.SectionNumber-1].VirtualAddress + } + + idxToSym[i] = sym + + // If this symbol is a function, it must retrieve its size from + // its AUX_FCN entry. + // It can happen that a function symbol doesn't have any AUX_FCN. + // In this case, needAuxFcn is false and their size will be set to 0. + if needAuxFcn { + switch f.TargetMachine { + case U802TOCMAGIC: + aux := new(AuxFcn32) + if err := binary.Read(sr, binary.BigEndian, aux); err != nil { + return nil, err + } + sym.AuxFcn.Size = int64(aux.Xfsize) + case U64_TOCMAGIC: + aux := new(AuxFcn64) + if err := binary.Read(sr, binary.BigEndian, aux); err != nil { + return nil, err + } + sym.AuxFcn.Size = int64(aux.Xfsize) + } + } + + // Read csect auxiliary entry (by convention, it is the last). + if !needAuxFcn { + if _, err := sr.Seek(int64(numaux-1)*SYMESZ, io.SeekCurrent); err != nil { + return nil, err + } + } + i += numaux + numaux = 0 + switch f.TargetMachine { + case U802TOCMAGIC: + aux := new(AuxCSect32) + if err := binary.Read(sr, binary.BigEndian, aux); err != nil { + return nil, err + } + sym.AuxCSect.SymbolType = int(aux.Xsmtyp & 0x7) + sym.AuxCSect.StorageMappingClass = int(aux.Xsmclas) + sym.AuxCSect.Length = int64(aux.Xscnlen) + case U64_TOCMAGIC: + aux := new(AuxCSect64) + if err := binary.Read(sr, binary.BigEndian, aux); err != nil { + return nil, err + } + sym.AuxCSect.SymbolType = int(aux.Xsmtyp & 0x7) + sym.AuxCSect.StorageMappingClass = int(aux.Xsmclas) + sym.AuxCSect.Length = int64(aux.Xscnlenhi)<<32 | int64(aux.Xscnlenlo) + } + f.Symbols = append(f.Symbols, sym) + skip: + i += numaux // Skip auxiliary entries + if _, err := sr.Seek(int64(numaux)*SYMESZ, io.SeekCurrent); err != nil { + return nil, err + } + } + + // Read relocations + // Only for .data or .text section + for sectNum, sect := range f.Sections { + if sect.Type != STYP_TEXT && sect.Type != STYP_DATA { + continue + } + if sect.Relptr == 0 { + continue + } + c := saferio.SliceCap[Reloc](uint64(sect.Nreloc)) + if c < 0 { + return nil, fmt.Errorf("too many relocs (%d) for section %d", sect.Nreloc, sectNum) + } + sect.Relocs = make([]Reloc, 0, c) + if _, err := sr.Seek(int64(sect.Relptr), io.SeekStart); err != nil { + return nil, err + } + for i := uint32(0); i < sect.Nreloc; i++ { + var reloc Reloc + switch f.TargetMachine { + case U802TOCMAGIC: + rel := new(Reloc32) + if err := binary.Read(sr, binary.BigEndian, rel); err != nil { + return nil, err + } + reloc.VirtualAddress = uint64(rel.Rvaddr) + reloc.Symbol = idxToSym[int(rel.Rsymndx)] + reloc.Type = rel.Rtype + reloc.Length = rel.Rsize&0x3F + 1 + + if rel.Rsize&0x80 != 0 { + reloc.Signed = true + } + if rel.Rsize&0x40 != 0 { + reloc.InstructionFixed = true + } + + case U64_TOCMAGIC: + rel := new(Reloc64) + if err := binary.Read(sr, binary.BigEndian, rel); err != nil { + return nil, err + } + reloc.VirtualAddress = rel.Rvaddr + reloc.Symbol = idxToSym[int(rel.Rsymndx)] + reloc.Type = rel.Rtype + reloc.Length = rel.Rsize&0x3F + 1 + if rel.Rsize&0x80 != 0 { + reloc.Signed = true + } + if rel.Rsize&0x40 != 0 { + reloc.InstructionFixed = true + } + } + + sect.Relocs = append(sect.Relocs, reloc) + } + } + + return f, nil +} + +type nobitsSectionReader struct{} + +func (*nobitsSectionReader) ReadAt(p []byte, off int64) (n int, err error) { + return 0, errors.New("unexpected read from section with uninitialized data") +} + +// Data reads and returns the contents of the XCOFF section s. +func (s *Section) Data() ([]byte, error) { + dat := make([]byte, s.sr.Size()) + n, err := s.sr.ReadAt(dat, 0) + if n == len(dat) { + err = nil + } + return dat[:n], err +} + +// CSect reads and returns the contents of a csect. +func (f *File) CSect(name string) []byte { + for _, sym := range f.Symbols { + if sym.Name == name && sym.AuxCSect.SymbolType == XTY_SD { + if i := sym.SectionNumber - 1; 0 <= i && i < len(f.Sections) { + s := f.Sections[i] + if sym.Value+uint64(sym.AuxCSect.Length) <= s.Size { + dat := make([]byte, sym.AuxCSect.Length) + _, err := s.sr.ReadAt(dat, int64(sym.Value)) + if err != nil { + return nil + } + return dat + } + } + break + } + } + return nil +} + +func (f *File) DWARF() (*dwarf.Data, error) { + // There are many other DWARF sections, but these + // are the ones the debug/dwarf package uses. + // Don't bother loading others. + var subtypes = [...]uint32{SSUBTYP_DWABREV, SSUBTYP_DWINFO, SSUBTYP_DWLINE, SSUBTYP_DWRNGES, SSUBTYP_DWSTR} + var dat [len(subtypes)][]byte + for i, subtype := range subtypes { + s := f.SectionByType(STYP_DWARF | subtype) + if s != nil { + b, err := s.Data() + if err != nil && uint64(len(b)) < s.Size { + return nil, err + } + dat[i] = b + } + } + + abbrev, info, line, ranges, str := dat[0], dat[1], dat[2], dat[3], dat[4] + return dwarf.New(abbrev, nil, nil, info, line, nil, ranges, str) +} + +// readImportID returns the import file IDs stored inside the .loader section. +// Library name pattern is either path/base/member or base/member +func (f *File) readImportIDs(s *Section) ([]string, error) { + // Read loader header + if _, err := s.sr.Seek(0, io.SeekStart); err != nil { + return nil, err + } + var istlen uint32 + var nimpid uint32 + var impoff uint64 + switch f.TargetMachine { + case U802TOCMAGIC: + lhdr := new(LoaderHeader32) + if err := binary.Read(s.sr, binary.BigEndian, lhdr); err != nil { + return nil, err + } + istlen = lhdr.Listlen + nimpid = lhdr.Lnimpid + impoff = uint64(lhdr.Limpoff) + case U64_TOCMAGIC: + lhdr := new(LoaderHeader64) + if err := binary.Read(s.sr, binary.BigEndian, lhdr); err != nil { + return nil, err + } + istlen = lhdr.Listlen + nimpid = lhdr.Lnimpid + impoff = lhdr.Limpoff + } + + // Read loader import file ID table + if _, err := s.sr.Seek(int64(impoff), io.SeekStart); err != nil { + return nil, err + } + table := make([]byte, istlen) + if _, err := io.ReadFull(s.sr, table); err != nil { + return nil, err + } + + offset := 0 + // First import file ID is the default LIBPATH value + libpath := cstring(table[offset:]) + f.LibraryPaths = strings.Split(libpath, ":") + offset += len(libpath) + 3 // 3 null bytes + all := make([]string, 0) + for i := 1; i < int(nimpid); i++ { + impidpath := cstring(table[offset:]) + offset += len(impidpath) + 1 + impidbase := cstring(table[offset:]) + offset += len(impidbase) + 1 + impidmem := cstring(table[offset:]) + offset += len(impidmem) + 1 + var path string + if len(impidpath) > 0 { + path = impidpath + "/" + impidbase + "/" + impidmem + } else { + path = impidbase + "/" + impidmem + } + all = append(all, path) + } + + return all, nil +} + +// ImportedSymbols returns the names of all symbols +// referred to by the binary f that are expected to be +// satisfied by other libraries at dynamic load time. +// It does not return weak symbols. +func (f *File) ImportedSymbols() ([]ImportedSymbol, error) { + s := f.SectionByType(STYP_LOADER) + if s == nil { + return nil, nil + } + // Read loader header + if _, err := s.sr.Seek(0, io.SeekStart); err != nil { + return nil, err + } + var stlen uint32 + var stoff uint64 + var nsyms uint32 + var symoff uint64 + switch f.TargetMachine { + case U802TOCMAGIC: + lhdr := new(LoaderHeader32) + if err := binary.Read(s.sr, binary.BigEndian, lhdr); err != nil { + return nil, err + } + stlen = lhdr.Lstlen + stoff = uint64(lhdr.Lstoff) + nsyms = lhdr.Lnsyms + symoff = LDHDRSZ_32 + case U64_TOCMAGIC: + lhdr := new(LoaderHeader64) + if err := binary.Read(s.sr, binary.BigEndian, lhdr); err != nil { + return nil, err + } + stlen = lhdr.Lstlen + stoff = lhdr.Lstoff + nsyms = lhdr.Lnsyms + symoff = lhdr.Lsymoff + } + + // Read loader section string table + if _, err := s.sr.Seek(int64(stoff), io.SeekStart); err != nil { + return nil, err + } + st := make([]byte, stlen) + if _, err := io.ReadFull(s.sr, st); err != nil { + return nil, err + } + + // Read imported libraries + libs, err := f.readImportIDs(s) + if err != nil { + return nil, err + } + + // Read loader symbol table + if _, err := s.sr.Seek(int64(symoff), io.SeekStart); err != nil { + return nil, err + } + all := make([]ImportedSymbol, 0) + for i := 0; i < int(nsyms); i++ { + var name string + var ifile uint32 + var ok bool + switch f.TargetMachine { + case U802TOCMAGIC: + ldsym := new(LoaderSymbol32) + if err := binary.Read(s.sr, binary.BigEndian, ldsym); err != nil { + return nil, err + } + if ldsym.Lsmtype&0x40 == 0 { + continue // Imported symbols only + } + zeroes := binary.BigEndian.Uint32(ldsym.Lname[:4]) + if zeroes != 0 { + name = cstring(ldsym.Lname[:]) + } else { + offset := binary.BigEndian.Uint32(ldsym.Lname[4:]) + name, ok = getString(st, offset) + if !ok { + continue + } + } + ifile = ldsym.Lifile + case U64_TOCMAGIC: + ldsym := new(LoaderSymbol64) + if err := binary.Read(s.sr, binary.BigEndian, ldsym); err != nil { + return nil, err + } + if ldsym.Lsmtype&0x40 == 0 { + continue // Imported symbols only + } + name, ok = getString(st, ldsym.Loffset) + if !ok { + continue + } + ifile = ldsym.Lifile + } + var sym ImportedSymbol + sym.Name = name + if ifile >= 1 && int(ifile) <= len(libs) { + sym.Library = libs[ifile-1] + } + all = append(all, sym) + } + + return all, nil +} + +// ImportedLibraries returns the names of all libraries +// referred to by the binary f that are expected to be +// linked with the binary at dynamic link time. +func (f *File) ImportedLibraries() ([]string, error) { + s := f.SectionByType(STYP_LOADER) + if s == nil { + return nil, nil + } + all, err := f.readImportIDs(s) + return all, err +} diff --git a/internal/go/src/xcoff/xcoff.go b/internal/go/src/xcoff/xcoff.go new file mode 100755 index 0000000..db81542 --- /dev/null +++ b/internal/go/src/xcoff/xcoff.go @@ -0,0 +1,367 @@ +// Copyright 2018 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package xcoff + +// File Header. +type FileHeader32 struct { + Fmagic uint16 // Target machine + Fnscns uint16 // Number of sections + Ftimedat uint32 // Time and date of file creation + Fsymptr uint32 // Byte offset to symbol table start + Fnsyms uint32 // Number of entries in symbol table + Fopthdr uint16 // Number of bytes in optional header + Fflags uint16 // Flags +} + +type FileHeader64 struct { + Fmagic uint16 // Target machine + Fnscns uint16 // Number of sections + Ftimedat uint32 // Time and date of file creation + Fsymptr uint64 // Byte offset to symbol table start + Fopthdr uint16 // Number of bytes in optional header + Fflags uint16 // Flags + Fnsyms uint32 // Number of entries in symbol table +} + +const ( + FILHSZ_32 = 20 + FILHSZ_64 = 24 +) +const ( + U802TOCMAGIC = 0737 // AIX 32-bit XCOFF + U64_TOCMAGIC = 0767 // AIX 64-bit XCOFF +) + +// Flags that describe the type of the object file. +const ( + F_RELFLG = 0x0001 + F_EXEC = 0x0002 + F_LNNO = 0x0004 + F_FDPR_PROF = 0x0010 + F_FDPR_OPTI = 0x0020 + F_DSA = 0x0040 + F_VARPG = 0x0100 + F_DYNLOAD = 0x1000 + F_SHROBJ = 0x2000 + F_LOADONLY = 0x4000 +) + +// Section Header. +type SectionHeader32 struct { + Sname [8]byte // Section name + Spaddr uint32 // Physical address + Svaddr uint32 // Virtual address + Ssize uint32 // Section size + Sscnptr uint32 // Offset in file to raw data for section + Srelptr uint32 // Offset in file to relocation entries for section + Slnnoptr uint32 // Offset in file to line number entries for section + Snreloc uint16 // Number of relocation entries + Snlnno uint16 // Number of line number entries + Sflags uint32 // Flags to define the section type +} + +type SectionHeader64 struct { + Sname [8]byte // Section name + Spaddr uint64 // Physical address + Svaddr uint64 // Virtual address + Ssize uint64 // Section size + Sscnptr uint64 // Offset in file to raw data for section + Srelptr uint64 // Offset in file to relocation entries for section + Slnnoptr uint64 // Offset in file to line number entries for section + Snreloc uint32 // Number of relocation entries + Snlnno uint32 // Number of line number entries + Sflags uint32 // Flags to define the section type + Spad uint32 // Needs to be 72 bytes long +} + +// Flags defining the section type. +const ( + STYP_DWARF = 0x0010 + STYP_TEXT = 0x0020 + STYP_DATA = 0x0040 + STYP_BSS = 0x0080 + STYP_EXCEPT = 0x0100 + STYP_INFO = 0x0200 + STYP_TDATA = 0x0400 + STYP_TBSS = 0x0800 + STYP_LOADER = 0x1000 + STYP_DEBUG = 0x2000 + STYP_TYPCHK = 0x4000 + STYP_OVRFLO = 0x8000 +) +const ( + SSUBTYP_DWINFO = 0x10000 // DWARF info section + SSUBTYP_DWLINE = 0x20000 // DWARF line-number section + SSUBTYP_DWPBNMS = 0x30000 // DWARF public names section + SSUBTYP_DWPBTYP = 0x40000 // DWARF public types section + SSUBTYP_DWARNGE = 0x50000 // DWARF aranges section + SSUBTYP_DWABREV = 0x60000 // DWARF abbreviation section + SSUBTYP_DWSTR = 0x70000 // DWARF strings section + SSUBTYP_DWRNGES = 0x80000 // DWARF ranges section + SSUBTYP_DWLOC = 0x90000 // DWARF location lists section + SSUBTYP_DWFRAME = 0xA0000 // DWARF frames section + SSUBTYP_DWMAC = 0xB0000 // DWARF macros section +) + +// Symbol Table Entry. +type SymEnt32 struct { + Nname [8]byte // Symbol name + Nvalue uint32 // Symbol value + Nscnum uint16 // Section number of symbol + Ntype uint16 // Basic and derived type specification + Nsclass uint8 // Storage class of symbol + Nnumaux uint8 // Number of auxiliary entries +} + +type SymEnt64 struct { + Nvalue uint64 // Symbol value + Noffset uint32 // Offset of the name in string table or .debug section + Nscnum uint16 // Section number of symbol + Ntype uint16 // Basic and derived type specification + Nsclass uint8 // Storage class of symbol + Nnumaux uint8 // Number of auxiliary entries +} + +const SYMESZ = 18 + +const ( + // Nscnum + N_DEBUG = -2 + N_ABS = -1 + N_UNDEF = 0 + + //Ntype + SYM_V_INTERNAL = 0x1000 + SYM_V_HIDDEN = 0x2000 + SYM_V_PROTECTED = 0x3000 + SYM_V_EXPORTED = 0x4000 + SYM_TYPE_FUNC = 0x0020 // is function +) + +// Storage Class. +const ( + C_NULL = 0 // Symbol table entry marked for deletion + C_EXT = 2 // External symbol + C_STAT = 3 // Static symbol + C_BLOCK = 100 // Beginning or end of inner block + C_FCN = 101 // Beginning or end of function + C_FILE = 103 // Source file name and compiler information + C_HIDEXT = 107 // Unnamed external symbol + C_BINCL = 108 // Beginning of include file + C_EINCL = 109 // End of include file + C_WEAKEXT = 111 // Weak external symbol + C_DWARF = 112 // DWARF symbol + C_GSYM = 128 // Global variable + C_LSYM = 129 // Automatic variable allocated on stack + C_PSYM = 130 // Argument to subroutine allocated on stack + C_RSYM = 131 // Register variable + C_RPSYM = 132 // Argument to function or procedure stored in register + C_STSYM = 133 // Statically allocated symbol + C_BCOMM = 135 // Beginning of common block + C_ECOML = 136 // Local member of common block + C_ECOMM = 137 // End of common block + C_DECL = 140 // Declaration of object + C_ENTRY = 141 // Alternate entry + C_FUN = 142 // Function or procedure + C_BSTAT = 143 // Beginning of static block + C_ESTAT = 144 // End of static block + C_GTLS = 145 // Global thread-local variable + C_STTLS = 146 // Static thread-local variable +) + +// File Auxiliary Entry +type AuxFile64 struct { + Xfname [8]byte // Name or offset inside string table + Xftype uint8 // Source file string type + Xauxtype uint8 // Type of auxiliary entry +} + +// Function Auxiliary Entry +type AuxFcn32 struct { + Xexptr uint32 // File offset to exception table entry + Xfsize uint32 // Size of function in bytes + Xlnnoptr uint32 // File pointer to line number + Xendndx uint32 // Symbol table index of next entry + Xpad uint16 // Unused +} +type AuxFcn64 struct { + Xlnnoptr uint64 // File pointer to line number + Xfsize uint32 // Size of function in bytes + Xendndx uint32 // Symbol table index of next entry + Xpad uint8 // Unused + Xauxtype uint8 // Type of auxiliary entry +} + +type AuxSect64 struct { + Xscnlen uint64 // section length + Xnreloc uint64 // Num RLDs + pad uint8 + Xauxtype uint8 // Type of auxiliary entry +} + +// csect Auxiliary Entry. +type AuxCSect32 struct { + Xscnlen uint32 // Length or symbol table index + Xparmhash uint32 // Offset of parameter type-check string + Xsnhash uint16 // .typchk section number + Xsmtyp uint8 // Symbol alignment and type + Xsmclas uint8 // Storage-mapping class + Xstab uint32 // Reserved + Xsnstab uint16 // Reserved +} + +type AuxCSect64 struct { + Xscnlenlo uint32 // Lower 4 bytes of length or symbol table index + Xparmhash uint32 // Offset of parameter type-check string + Xsnhash uint16 // .typchk section number + Xsmtyp uint8 // Symbol alignment and type + Xsmclas uint8 // Storage-mapping class + Xscnlenhi uint32 // Upper 4 bytes of length or symbol table index + Xpad uint8 // Unused + Xauxtype uint8 // Type of auxiliary entry +} + +// Auxiliary type +const ( + _AUX_EXCEPT = 255 + _AUX_FCN = 254 + _AUX_SYM = 253 + _AUX_FILE = 252 + _AUX_CSECT = 251 + _AUX_SECT = 250 +) + +// Symbol type field. +const ( + XTY_ER = 0 // External reference + XTY_SD = 1 // Section definition + XTY_LD = 2 // Label definition + XTY_CM = 3 // Common csect definition +) + +// Defines for File auxiliary definitions: x_ftype field of x_file +const ( + XFT_FN = 0 // Source File Name + XFT_CT = 1 // Compile Time Stamp + XFT_CV = 2 // Compiler Version Number + XFT_CD = 128 // Compiler Defined Information +) + +// Storage-mapping class. +const ( + XMC_PR = 0 // Program code + XMC_RO = 1 // Read-only constant + XMC_DB = 2 // Debug dictionary table + XMC_TC = 3 // TOC entry + XMC_UA = 4 // Unclassified + XMC_RW = 5 // Read/Write data + XMC_GL = 6 // Global linkage + XMC_XO = 7 // Extended operation + XMC_SV = 8 // 32-bit supervisor call descriptor + XMC_BS = 9 // BSS class + XMC_DS = 10 // Function descriptor + XMC_UC = 11 // Unnamed FORTRAN common + XMC_TC0 = 15 // TOC anchor + XMC_TD = 16 // Scalar data entry in the TOC + XMC_SV64 = 17 // 64-bit supervisor call descriptor + XMC_SV3264 = 18 // Supervisor call descriptor for both 32-bit and 64-bit + XMC_TL = 20 // Read/Write thread-local data + XMC_UL = 21 // Read/Write thread-local data (.tbss) + XMC_TE = 22 // TOC entry +) + +// Loader Header. +type LoaderHeader32 struct { + Lversion uint32 // Loader section version number + Lnsyms uint32 // Number of symbol table entries + Lnreloc uint32 // Number of relocation table entries + Listlen uint32 // Length of import file ID string table + Lnimpid uint32 // Number of import file IDs + Limpoff uint32 // Offset to start of import file IDs + Lstlen uint32 // Length of string table + Lstoff uint32 // Offset to start of string table +} + +type LoaderHeader64 struct { + Lversion uint32 // Loader section version number + Lnsyms uint32 // Number of symbol table entries + Lnreloc uint32 // Number of relocation table entries + Listlen uint32 // Length of import file ID string table + Lnimpid uint32 // Number of import file IDs + Lstlen uint32 // Length of string table + Limpoff uint64 // Offset to start of import file IDs + Lstoff uint64 // Offset to start of string table + Lsymoff uint64 // Offset to start of symbol table + Lrldoff uint64 // Offset to start of relocation entries +} + +const ( + LDHDRSZ_32 = 32 + LDHDRSZ_64 = 56 +) + +// Loader Symbol. +type LoaderSymbol32 struct { + Lname [8]byte // Symbol name or byte offset into string table + Lvalue uint32 // Address field + Lscnum uint16 // Section number containing symbol + Lsmtype uint8 // Symbol type, export, import flags + Lsmclas uint8 // Symbol storage class + Lifile uint32 // Import file ID; ordinal of import file IDs + Lparm uint32 // Parameter type-check field +} + +type LoaderSymbol64 struct { + Lvalue uint64 // Address field + Loffset uint32 // Byte offset into string table of symbol name + Lscnum uint16 // Section number containing symbol + Lsmtype uint8 // Symbol type, export, import flags + Lsmclas uint8 // Symbol storage class + Lifile uint32 // Import file ID; ordinal of import file IDs + Lparm uint32 // Parameter type-check field +} + +type Reloc32 struct { + Rvaddr uint32 // (virtual) address of reference + Rsymndx uint32 // Index into symbol table + Rsize uint8 // Sign and reloc bit len + Rtype uint8 // Toc relocation type +} + +type Reloc64 struct { + Rvaddr uint64 // (virtual) address of reference + Rsymndx uint32 // Index into symbol table + Rsize uint8 // Sign and reloc bit len + Rtype uint8 // Toc relocation type +} + +const ( + R_POS = 0x00 // A(sym) Positive Relocation + R_NEG = 0x01 // -A(sym) Negative Relocation + R_REL = 0x02 // A(sym-*) Relative to self + R_TOC = 0x03 // A(sym-TOC) Relative to TOC + R_TRL = 0x12 // A(sym-TOC) TOC Relative indirect load. + + R_TRLA = 0x13 // A(sym-TOC) TOC Rel load address. modifiable inst + R_GL = 0x05 // A(external TOC of sym) Global Linkage + R_TCL = 0x06 // A(local TOC of sym) Local object TOC address + R_RL = 0x0C // A(sym) Pos indirect load. modifiable instruction + R_RLA = 0x0D // A(sym) Pos Load Address. modifiable instruction + R_REF = 0x0F // AL0(sym) Non relocating ref. No garbage collect + R_BA = 0x08 // A(sym) Branch absolute. Cannot modify instruction + R_RBA = 0x18 // A(sym) Branch absolute. modifiable instruction + R_BR = 0x0A // A(sym-*) Branch rel to self. non modifiable + R_RBR = 0x1A // A(sym-*) Branch rel to self. modifiable instr + + R_TLS = 0x20 // General-dynamic reference to TLS symbol + R_TLS_IE = 0x21 // Initial-exec reference to TLS symbol + R_TLS_LD = 0x22 // Local-dynamic reference to TLS symbol + R_TLS_LE = 0x23 // Local-exec reference to TLS symbol + R_TLSM = 0x24 // Module reference to TLS symbol + R_TLSML = 0x25 // Module reference to local (own) module + + R_TOCU = 0x30 // Relative to TOC - high order bits + R_TOCL = 0x31 // Relative to TOC - low order bits +) diff --git a/internal/go/update.go b/internal/go/update.go new file mode 100644 index 0000000..c11fc5c --- /dev/null +++ b/internal/go/update.go @@ -0,0 +1,78 @@ +package main + +//go:generate go run update.go + +import ( + "bytes" + "io/fs" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func main() { + must0(os.RemoveAll("src")) + must0(os.Mkdir("src", 0755)) + + goroot := strings.TrimSpace(must(run("go", "env", "GOROOT"))) + for _, subdir := range []string{"archive", "bio", "goobj", "objabi", "objfile", "src", "sys", "disasm"} { + dir := filepath.Join(goroot, "src/cmd/internal", subdir) + copydir(dir, filepath.Join("src", subdir)) + } + for _, subdir := range []string{"abi", "goarch", "bisect", "buildcfg", "goexperiment", "saferio", "unsafeheader", "xcoff"} { + dir := filepath.Join(goroot, "src/internal", subdir) + copydir(dir, filepath.Join("src", subdir)) + } + + must0(os.WriteFile("src/disasm/expose.go", must(os.ReadFile("expose.go_")), 0644)) + must0(os.Remove("src/abi/abi_test.s")) +} + +func copydir(srcdir, dstdir string) { + must0(os.MkdirAll(dstdir, 0755)) + must0(filepath.WalkDir(srcdir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + base := must(filepath.Rel(srcdir, path)) + + if d.IsDir() { + if d.Name() == "testdata" { + return fs.SkipDir + } + return os.MkdirAll(filepath.Join(dstdir, base), 0755) + } + if strings.HasSuffix(d.Name(), "_test.go") { + return nil + } + + dstfile := filepath.Join(dstdir, base) + data := must(os.ReadFile(path)) + + data = bytes.ReplaceAll(data, []byte(` "cmd/internal/`), []byte(` "loov.dev/lensm/internal/go/src/`)) + data = bytes.ReplaceAll(data, []byte(`import "cmd/internal/`), []byte(`import "loov.dev/lensm/internal/go/src/`)) + data = bytes.ReplaceAll(data, []byte(` "internal/`), []byte(` "loov.dev/lensm/internal/go/src/`)) + data = bytes.ReplaceAll(data, []byte(`import "internal/`), []byte(`import "loov.dev/lensm/internal/go/src/`)) + + return os.WriteFile(dstfile, data, 0755) + })) +} + +func run(cmd string, args ...string) (string, error) { + out, err := exec.Command(cmd, args...).Output() + return string(out), err +} + +func must0(err error) { + if err != nil { + panic(err) + } +} + +func must[T any](v T, err error) T { + if err != nil { + panic(err) + } + return v +} diff --git a/internal/goobj/code.go b/internal/goobj/code.go new file mode 100644 index 0000000..b491a10 --- /dev/null +++ b/internal/goobj/code.go @@ -0,0 +1,254 @@ +package goobj + +import ( + "fmt" + "os" + "regexp" + "sort" + "strconv" + "strings" + + "loov.dev/lensm/internal/disasm" + godisasm "loov.dev/lensm/internal/go/src/disasm" +) + +var rxRefAbs = regexp.MustCompile(`\s0x[\da-fA-F]+$`) +var rxRefRel = regexp.MustCompile(`\s-?\d+\(PC\)$`) +var rxCallOrJump = regexp.MustCompile(`^(?:CALL|JMP)\s+(.+?)\(SB\)`) + +// Disassemble disassembles the specified symbol. +func Disassemble(dis *godisasm.Disasm, sym *Func, opts disasm.Options) (*disasm.Code, error) { + neededLines := make(map[string]*disasm.LineSet) + + file, _, _ := dis.PCLN().PCToLine(sym.sym.Addr) + needRefPCs := map[uint64]struct{}{} + + code := &disasm.Code{ + Name: sym.Name(), + File: file, + Arch: dis.GOARCH(), + } + var instructions []disasm.Inst + dis.DecodeSyntax(sym.sym.Addr, sym.sym.Addr+uint64(sym.sym.Size), sym.sym.Relocs, + func(pc, size uint64, file string, line int, text, nativeText, mnemonic string) { + // TODO: find a better way to calculate the jump target + var refPC uint64 + var call string + if match := rxRefAbs.FindString(text); match != "" { + if target, err := strconv.ParseInt(match[3:], 16, 64); err == nil { + refPC = uint64(target) + } + } else if match := rxRefRel.FindString(text); match != "" { + // TODO: this calculation seems incorrect + if target, err := strconv.ParseInt(match[1:len(match)-4], 10, 64); err == nil { + refPC = uint64(int64(pc) + target*4) + } else { + panic(err) + } + } else if match := rxCallOrJump.FindStringSubmatch(text); len(match) > 0 { + call = match[1] + } + + if refPC != 0 { + needRefPCs[refPC] = struct{}{} + } + instructions = append(instructions, disasm.Inst{ + PC: pc, + Text: text, + NativeText: nativeText, + Mnemonic: mnemonic, + File: file, + Line: line, + Call: call, + RefPC: refPC, + }) + + if file != "" && file != "" { + lineset, ok := neededLines[file] + if !ok { + lineset = &disasm.LineSet{} + neededLines[file] = lineset + } + lineset.Add(line) + } + }) + + pcToIndex := map[uint64]int{} + for _, ix := range instructions { + if _, ok := needRefPCs[ix.PC]; ok { + // add empty line + code.Insts = append(code.Insts, disasm.Inst{}) + } + pcToIndex[ix.PC] = len(code.Insts) + code.Insts = append(code.Insts, ix) + } + + type jumpInterval struct { + index int + ix *disasm.Inst + min, max uint64 + } + + var jumps []jumpInterval + for i := range code.Insts { + ix := &code.Insts[i] + if ix.RefPC != 0 { + target, ok := pcToIndex[ix.RefPC] + if !ok { + continue + } + ix.RefOffset = target - i + + if ix.PC <= ix.RefPC { + jumps = append(jumps, jumpInterval{ + index: i, + ix: ix, + min: ix.PC, + max: ix.RefPC, + }) + } else { + jumps = append(jumps, jumpInterval{ + index: i, + ix: ix, + min: ix.RefPC, + max: ix.PC, + }) + } + } + } + + sort.Slice(jumps, func(i, k int) bool { + if jumps[i].min == jumps[k].min { + return jumps[i].max > jumps[k].max + } + return jumps[i].min < jumps[k].min + }) + + var stackLayers []uint64 + insertToStack := func(ix *disasm.Inst, max uint64) { + found := false + for k, pc := range stackLayers { + if pc == 0 { + stackLayers[k] = max + ix.RefStack = k + found = true + break + } + } + if !found { + code.MaxJump = len(stackLayers) + ix.RefStack = len(stackLayers) + stackLayers = append(stackLayers, max) + } + } + + for _, jump := range jumps { + for i, pc := range stackLayers { + if pc <= jump.min { + stackLayers[i] = 0 + } + } + insertToStack(jump.ix, jump.max) + } + for i := range code.Insts { + ix := &code.Insts[i] + ix.RefStack = code.MaxJump - ix.RefStack + 1 + } + code.MaxJump++ + + // remove trailing interrupts from funcs + for len(code.Insts) > 0 && + (strings.HasPrefix(code.Insts[len(code.Insts)-1].Text, "INT ") || + code.Insts[len(code.Insts)-1].Text == "?") { + code.Insts = code.Insts[:len(code.Insts)-1] + } + + // load sources + code.Source = LoadSources(neededLines, code.File, opts.Context) + + // create a mapping from source code to disassembly + type fileLine struct { + file string + line int + } + + lineRefs := map[fileLine]*disasm.LineSet{} + for i, ix := range code.Insts { + k := fileLine{file: ix.File, line: ix.Line} + n, ok := lineRefs[k] + if !ok { + n = &disasm.LineSet{} + lineRefs[k] = n + } + n.Add(i) + } + for i := range code.Source { + src := &code.Source[i] + for k := range src.Blocks { + block := &src.Blocks[k] + block.Related = make([][]disasm.LineRange, len(block.Lines)) + for line := block.From; line <= block.To; line++ { // todo check: line <= block.To + if refs, ok := lineRefs[fileLine{file: src.File, line: line}]; ok { + block.Related[line-block.From] = refs.RangesZero() + } + } + } + } + + return code, nil +} + +var rxEnvVariable = regexp.MustCompile(`\$[a-zA-Z_]+[a-zA-Z0-9_]+\b`) + +func replaceEnvironmentVariables(s string) string { + return rxEnvVariable.ReplaceAllStringFunc(s, func(env string) string { + replacement := os.Getenv(env[1:]) + if replacement != "" { + return replacement + } + return env + }) +} + +// LoadSources loads the specified line sets. +func LoadSources(needed map[string]*disasm.LineSet, symbolFile string, context int) []disasm.Source { + var sources []disasm.Source + for file, set := range needed { + data, err := os.ReadFile(replaceEnvironmentVariables(file)) + if err != nil { + // TODO: should we create a stub source block instead? + fmt.Fprintf(os.Stderr, "unable to load source from %q: %v\n", file, err) + continue + } + lines := strings.Split(string(data), "\n") + source := disasm.Source{ + File: file, + } + for _, r := range set.Ranges(context) { + to := min(r.To-1, len(lines)) + lineBlock := lines[r.From-1 : to] + for i, v := range lineBlock { + lineBlock[i] = strings.Replace(v, "\t", " ", -1) + } + + source.Blocks = append(source.Blocks, disasm.SourceBlock{ + LineRange: r, + Lines: lineBlock, + }) + } + sources = append(sources, source) + } + + // Sort the sources and prioritize the file where the main symbol is located. + sort.Slice(sources, func(i, j int) bool { + if sources[i].File == symbolFile { + return true + } + if sources[j].File == symbolFile { + return false + } + return sources[i].File < sources[j].File + }) + + return sources +} diff --git a/internal/goobj/file.go b/internal/goobj/file.go new file mode 100644 index 0000000..d93cb58 --- /dev/null +++ b/internal/goobj/file.go @@ -0,0 +1,119 @@ +package goobj + +import ( + "regexp" + "sort" + "strings" + "sync" + + "loov.dev/lensm/internal/disasm" + godisasm "loov.dev/lensm/internal/go/src/disasm" + "loov.dev/lensm/internal/go/src/objfile" +) + +var _ disasm.File = (*File)(nil) +var _ disasm.Func = (*Func)(nil) + +// File contains information about the object file. +type File struct { + objfile *objfile.File + disasm *godisasm.Disasm + funcs []disasm.Func + + // mu guards cache and serializes Disassemble calls: disassembly + // lazily populates line-table caches inside disasm, which is not + // safe for concurrent use. + mu sync.Mutex + cache map[cacheKey]cacheEntry +} + +// cacheKey includes the options: MCP callers choose the source context +// per call, and a cache keyed by function alone would silently serve +// whichever context happened to load first. +type cacheKey struct { + fn *Func + context int +} + +// cacheEntry also caches failures, so an erroring function isn't +// re-disassembled on every frame. +type cacheEntry struct { + code *disasm.Code + err error +} + +func (file *File) Funcs() []disasm.Func { return file.funcs } + +// Function contains information about the executable. +type Func struct { + obj *File + sym objfile.Sym + + sortName string +} + +func (fn *Func) Name() string { return fn.sym.Name } + +func (file *File) Close() error { + return file.objfile.Close() +} + +func Load(path string) (*File, error) { + f, err := objfile.Open(path) + if err != nil { + return nil, err + } + + dis, err := godisasm.DisasmForFile(f) + if err != nil { + _ = f.Close() + return nil, err + } + + file := &File{ + objfile: f, + disasm: dis, + cache: make(map[cacheKey]cacheEntry), + } + + for _, sym := range dis.Syms() { + if sym.Code != 'T' && sym.Code != 't' || sym.Addr < dis.TextStart() { + continue + } + sym := &Func{ + obj: file, + sym: sym, + sortName: sortingName(sym.Name), + } + file.funcs = append(file.funcs, sym) + } + + sort.SliceStable(file.funcs, func(i, k int) bool { + return sortingName(file.funcs[i].Name()) < sortingName(file.funcs[k].Name()) + }) + + return file, nil +} + +func (fn *Func) Load(opts disasm.Options) (*disasm.Code, error) { + return fn.obj.LoadCode(fn, opts) +} + +func (file *File) LoadCode(fn *Func, opts disasm.Options) (*disasm.Code, error) { + file.mu.Lock() + defer file.mu.Unlock() + key := cacheKey{fn: fn, context: opts.Context} + entry, ok := file.cache[key] + if !ok { + entry.code, entry.err = Disassemble(fn.obj.disasm, fn, opts) + file.cache[key] = entry + } + return entry.code, entry.err +} + +var rxCodeDelimiter = regexp.MustCompile(`[ *().]+`) + +func sortingName(sym string) string { + sym = strings.ToLower(sym) + return rxCodeDelimiter.ReplaceAllString(sym, " ") +} diff --git a/internal/goobj/file_test.go b/internal/goobj/file_test.go new file mode 100644 index 0000000..d06eb27 --- /dev/null +++ b/internal/goobj/file_test.go @@ -0,0 +1,55 @@ +package goobj + +import ( + "os" + "os/exec" + "path/filepath" + "sync" + "testing" + + "loov.dev/lensm/internal/disasm" +) + +func TestLoadCode_ConcurrentCallsShareCache(t *testing.T) { + if testing.Short() { + t.Skip("builds a test binary") + } + + dir := t.TempDir() + src := filepath.Join(dir, "main.go") + err := os.WriteFile(src, []byte(`package main + +func main() { println(add(1, 2)) } + +//go:noinline +func add(a, b int) int { return a + b } +`), 0o644) + if err != nil { + t.Fatal(err) + } + bin := filepath.Join(dir, "example.exe") + if out, err := exec.Command("go", "build", "-o", bin, src).CombinedOutput(); err != nil { + t.Fatalf("go build: %v\n%s", err, out) + } + + file, err := Load(bin) + if err != nil { + t.Fatal(err) + } + t.Cleanup(func() { _ = file.Close() }) + + funcs := file.Funcs() + if len(funcs) > 32 { + funcs = funcs[:32] + } + + var wg sync.WaitGroup + for range 8 { + wg.Go(func() { + for _, fn := range funcs { + _, _ = fn.Load(disasm.Options{Context: 1}) + } + }) + } + wg.Wait() +} diff --git a/internal/gui/filterlist.go b/internal/gui/filterlist.go new file mode 100644 index 0000000..6e78399 --- /dev/null +++ b/internal/gui/filterlist.go @@ -0,0 +1,144 @@ +package gui + +import ( + "fmt" + "regexp" + + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" +) + +type FilterListItem interface { + Name() string +} + +// FilterList lists symbols for filtering and selection. +type FilterList[T FilterListItem] struct { + All []T + Filter widget.Editor + FilterError string + Filtered []T + + Selected string + SelectedItem T + + List SelectList +} + +// NewFilterList creates a new list with the specified theme. +func NewFilterList[T FilterListItem](theme *Theme) *FilterList[T] { + ui := &FilterList[T]{} + ui.Filter.SingleLine = true + ui.List = NewVerticalSelectList(unit.Dp(theme.TextSize) + 4) + return ui +} + +// SelectIndex selects the specified item. +func (ui *FilterList[T]) SelectIndex(index int) { + if !InRange(index, len(ui.Filtered)) { + return + } + + ui.List.Selected = index + ui.Selected = ui.Filtered[index].Name() + ui.SelectedItem = ui.Filtered[index] +} + +// SetItems updates the full list. +func (ui *FilterList[T]) SetItems(all []T) { + ui.All = all + ui.updateFiltered() + if ui.List.Selected == -1 && len(ui.Filtered) > 0 { + ui.SelectIndex(0) + } +} + +// SetFilter sets the filter. +func (ui *FilterList[T]) SetFilter(filter string) { + ui.Filter.SetText(filter) + ui.updateFiltered() +} + +// updateFiltered updates the filtered list from the unfiltered content. +func (ui *FilterList[T]) updateFiltered() { + defer func() { + ui.List.Selected = -1 + var zero T + ui.SelectedItem = zero + for i, item := range ui.Filtered { + if item.Name() == ui.Selected { + ui.List.Selected = i + ui.SelectedItem = item + // TODO, maybe scroll into view? + break + } + } + }() + + rx, err := regexp.Compile("(?i)" + ui.Filter.Text()) + ui.FilterError = "" + if err != nil { + ui.FilterError = err.Error() + return + } + + ui.Filtered = ui.Filtered[:0] + for _, item := range ui.All { + if rx.MatchString(item.Name()) { + ui.Filtered = append(ui.Filtered, item) + } + } +} + +// Layout draws the list. +func (ui *FilterList[T]) Layout(th *Theme, gtx layout.Context) layout.Dimensions { + paint.FillShape(gtx.Ops, th.Colors.SecondaryBackground, clip.Rect{Max: gtx.Constraints.Min}.Op()) + + ui.SelectIndex(ui.List.Selected) + + changed := false + for { + ev, ok := ui.Filter.Update(gtx) + if !ok { + break + } + if _, ok := ev.(widget.ChangeEvent); ok { + changed = true + } + } + + if changed { + ui.updateFiltered() + gtx.Execute(op.InvalidateCmd{}) + } + + return layout.Flex{ + Axis: layout.Vertical, + }.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return FocusBorder(th.Theme, gtx.Focused(&ui.Filter)).Layout(gtx, + material.Editor(th.Theme, &ui.Filter, "Filter (regexp)").Layout) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + if ui.FilterError == "" { + return layout.Dimensions{} + } + return th.ErrorLabel(ui.FilterError, 1).Layout(gtx) + }), + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return ui.List.Layout(th.Theme, gtx, len(ui.Filtered), + StringListItem(th.Theme, &ui.List, func(index int) string { + return ui.Filtered[index].Name() + })) + }), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + body := th.Label(fmt.Sprintf("%d / %d", len(ui.Filtered), len(ui.All)), 0.8) + return layout.Center.Layout(gtx, body.Layout) + }), + ) +} diff --git a/internal/gui/focus.go b/internal/gui/focus.go new file mode 100644 index 0000000..457ec26 --- /dev/null +++ b/internal/gui/focus.go @@ -0,0 +1,41 @@ +package gui + +import ( + "image/color" + + "gioui.org/layout" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" +) + +// FocusBorderStyle implements a styling a focused widget. +type FocusBorderStyle struct { + Focused bool + BorderWidth unit.Dp + Color color.NRGBA +} + +// FocusBorder creates a focus border for a focused widget. +func FocusBorder(th *material.Theme, focused bool) FocusBorderStyle { + return FocusBorderStyle{ + Focused: focused, + BorderWidth: unit.Dp(2), + Color: th.ContrastBg, + } +} + +// Layout adds a focus border and styling. +func (focus FocusBorderStyle) Layout(gtx layout.Context, w layout.Widget) layout.Dimensions { + inset := layout.UniformInset(focus.BorderWidth) + if !focus.Focused { + return inset.Layout(gtx, w) + } + + return widget.Border{ + Color: focus.Color, + Width: focus.BorderWidth, + }.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return inset.Layout(gtx, w) + }) +} diff --git a/internal/gui/geom.go b/internal/gui/geom.go new file mode 100644 index 0000000..c230087 --- /dev/null +++ b/internal/gui/geom.go @@ -0,0 +1,22 @@ +package gui + +type Bounds struct{ Min, Max float32 } + +func BoundsWidth(min, width int) Bounds { + return Bounds{Min: float32(min), Max: float32(min + width)} +} + +func (b Bounds) Width() float32 { return b.Max - b.Min } + +func (b Bounds) Lerp(p float32) float32 { + return b.Min + p*(b.Max-b.Min) +} + +func (b Bounds) Contains(v float32) bool { + return b.Min <= v && v <= b.Max +} + +// InRange checks whether v is in bounds for length. +func InRange(v int, length int) bool { + return 0 <= v && v < length +} diff --git a/internal/gui/scroll.go b/internal/gui/scroll.go new file mode 100644 index 0000000..4d55a3c --- /dev/null +++ b/internal/gui/scroll.go @@ -0,0 +1,68 @@ +package gui + +import ( + "image" + + "gioui.org/gesture" + "gioui.org/io/pointer" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/widget" + "gioui.org/widget/material" +) + +// ScrollRegion is the vertical scroll state for a manually drawn +// column: wheel/gesture scrolling, a scrollbar, and animated jumps. +// Offset is the pixel offset of the content top relative to the +// viewport top, so it goes negative as the content scrolls up. +type ScrollRegion struct { + Offset float32 + Gesture gesture.Scroll + Bar widget.Scrollbar + Anim ScrollAnimation +} + +// LayoutBar draws a vertical scrollbar at barX with the given width, +// applies scrollbar drags and scroll gestures, and clamps the offset +// so the viewport stays within [contentTop, contentBot]. +func (region *ScrollRegion) LayoutBar(gtx layout.Context, th *material.Theme, barX, barWidth int, contentTop, contentBot float32) { + viewTop := -region.Offset + viewBot := -region.Offset + float32(gtx.Constraints.Max.Y) + + stack := op.Offset(image.Pt(barX, 0)).Push(gtx.Ops) + bgtx := gtx + bgtx.Constraints = layout.Exact(image.Pt(barWidth, gtx.Constraints.Max.Y)) + material.Scrollbar(th, ®ion.Bar).Layout(bgtx, layout.Vertical, + (viewTop-contentTop)/(contentBot-contentTop), + (viewBot-contentTop)/(contentBot-contentTop), + ) + stack.Pop() + + if distance := region.Bar.ScrollDistance(); distance != 0 { + region.Offset -= distance * (contentBot - contentTop) + } + if distance := region.Gesture.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Vertical, + pointer.ScrollRange{}, + pointer.ScrollRange{Min: -1000, Max: 1000}, + ); distance != 0 { + region.Offset -= float32(distance) + } + region.Clamp(gtx, contentTop, contentBot) +} + +// Clamp keeps the viewport within the content, stopping any running +// jump animation when it hits an edge. +func (region *ScrollRegion) Clamp(gtx layout.Context, contentTop, contentBot float32) { + if -region.Offset < contentTop { + region.Offset = -contentTop + region.Anim.Stop() + } + if -region.Offset+float32(gtx.Constraints.Max.Y) > contentBot { + if contentBot < float32(gtx.Constraints.Max.Y) { + region.Offset = -contentTop + } else { + region.Offset = float32(gtx.Constraints.Max.Y) - contentBot + } + region.Anim.Stop() + } +} diff --git a/internal/gui/selectlist.go b/internal/gui/selectlist.go new file mode 100644 index 0000000..db882cd --- /dev/null +++ b/internal/gui/selectlist.go @@ -0,0 +1,208 @@ +package gui + +import ( + "image" + "image/color" + + "gioui.org/f32" + "gioui.org/font" + "gioui.org/io/event" + "gioui.org/io/key" + "gioui.org/io/pointer" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" +) + +// NewVerticalSelectList creates a new select list with the specified item height. +func NewVerticalSelectList(itemHeight unit.Dp) SelectList { + return SelectList{ + List: widget.List{ + List: layout.List{ + Axis: layout.Vertical, + }, + }, + ItemHeight: itemHeight, + } +} + +// SelectList draws a list where items can be selected. +type SelectList struct { + widget.List + + Selected int + Hovered int + + ItemHeight unit.Dp +} + +// Layout draws the list. +func (list *SelectList) Layout(th *material.Theme, gtx layout.Context, length int, element layout.ListElement) layout.Dimensions { + return FocusBorder(th, gtx.Focused(list)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + size := gtx.Constraints.Max + gtx.Constraints = layout.Exact(size) + defer clip.Rect{Max: size}.Push(gtx.Ops).Pop() + + event.Op(gtx.Ops, list) + + changed := false + grabbed := false + + itemHeight := gtx.Metric.Dp(list.ItemHeight) + if itemHeight == 0 { + itemHeight = gtx.Metric.Sp(th.TextSize) + } + + pointerClicked := false + pointerHovered := false + pointerPosition := f32.Point{} + for { + // TODO: fix navigation when in filter. + ev, ok := gtx.Event( + key.FocusFilter{Target: list}, + key.Filter{Focus: list, Name: key.NameUpArrow}, + key.Filter{Focus: list, Name: key.NameDownArrow}, + key.Filter{Focus: list, Name: key.NameHome}, + key.Filter{Focus: list, Name: key.NameEnd}, + key.Filter{Focus: list, Name: key.NamePageUp}, + key.Filter{Focus: list, Name: key.NamePageDown}, + pointer.Filter{ + Target: list, + Kinds: pointer.Press | pointer.Move, + }, + ) + if !ok { + break + } + + switch ev := ev.(type) { + case key.Event: + if ev.State == key.Press { + offset := 0 + switch ev.Name { + case key.NameHome: + offset = -length + case key.NameEnd: + offset = length + case key.NameUpArrow: + offset = -1 + case key.NameDownArrow: + offset = 1 + case key.NamePageUp: + offset = -list.List.Position.Count + case key.NamePageDown: + offset = list.List.Position.Count + } + + if offset != 0 { + target := max(list.Selected+offset, 0) + if target >= length { + target = length - 1 + } + if list.Selected != target { + list.Selected = target + changed = true + } + } + + // if we get input and don't have a focus, then grab it + if !gtx.Focused(list) { + gtx.Execute(op.InvalidateCmd{}) + } + } + + case pointer.Event: + switch ev.Kind { + case pointer.Press: + if !gtx.Focused(list) && !grabbed { + grabbed = true + gtx.Execute(key.FocusCmd{Tag: list}) + } + pointerClicked = true + pointerPosition = ev.Position + case pointer.Move: + pointerHovered = true + pointerPosition = ev.Position + case pointer.Cancel: + list.Hovered = -1 + } + } + } + + if pointerClicked || pointerHovered { + // TODO: make this independent of fixed item height + clientClickY := list.Position.First*itemHeight + list.Position.Offset + int(pointerPosition.Y) + target := clientClickY / itemHeight + if 0 <= target && target <= length { + if pointerClicked && list.Selected != target { + list.Selected = target + } + if pointerHovered && list.Hovered != target { + list.Hovered = target + } + } + } + + if changed { + pos := &list.List.Position + switch { + case list.Selected < pos.First+1: + list.List.Position = layout.Position{First: list.Selected - 1} + case pos.First+pos.Count-1 <= list.Selected: + list.List.Position = layout.Position{First: list.Selected - pos.Count + 2} + } + } + + style := material.List(th, &list.List) + style.AnchorStrategy = material.Overlay + return style.Layout(gtx, length, + func(gtx layout.Context, index int) layout.Dimensions { + gtx.Constraints = layout.Exact(image.Point{ + X: gtx.Constraints.Max.X, + Y: itemHeight, + }) + return element(gtx, index) + }) + }) +} + +// StringListItem creates a string item drawer that reacts to hover and selection. +func StringListItem(th *material.Theme, state *SelectList, item func(int) string) layout.ListElement { + return func(gtx layout.Context, index int) layout.Dimensions { + defer clip.Rect{Max: gtx.Constraints.Max}.Push(gtx.Ops).Pop() + + bg := color.NRGBA{} + fg := th.Fg + weight := font.Normal + + switch { + case state.Selected == index: + if gtx.Focused(state) { + bg = th.ContrastBg + fg = th.ContrastFg + } + weight = font.Black + case state.Hovered == index: + bg = th.ContrastBg + bg.A /= 4 + } + + if bg != (color.NRGBA{}) { + paint.Fill(gtx.Ops, bg) + } + inset := layout.Inset{Top: 1, Right: 4, Bottom: 1, Left: 4} + return inset.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + label := material.Body1(th, item(index)) + label.Color = fg + label.MaxLines = 1 + label.TextSize = th.TextSize * 8 / 10 + label.Font.Weight = weight + gtx.Constraints.Max.X = MaxLineWidth + return label.Layout(gtx) + }) + } +} diff --git a/internal/gui/theme.go b/internal/gui/theme.go new file mode 100644 index 0000000..293e3ef --- /dev/null +++ b/internal/gui/theme.go @@ -0,0 +1,109 @@ +package gui + +import ( + "image/color" + + "gioui.org/unit" + "gioui.org/widget/material" + + "loov.dev/lensm/internal/syntax" +) + +// Theme bundles the material theme with the app palette so layout +// code takes one argument. Colors is recomputed by SetDark, not per +// frame. +type Theme struct { + *material.Theme + Colors UIColors +} + +func NewTheme(base *material.Theme, dark bool) *Theme { + th := &Theme{Theme: base} + th.SetDark(dark) + return th +} + +// SetDark switches the material palette and app colors between the +// dark and light schemes. +func (th *Theme) SetDark(dark bool) { + th.Colors = applyPalette(th.Theme, dark) +} + +// Label returns a body label in the app text color, scaled relative +// to the theme text size. +func (th *Theme) Label(text string, scale float32) material.LabelStyle { + label := material.Body1(th.Theme, text) + label.TextSize = th.TextSize * unit.Sp(scale) + label.Color = th.Colors.Text + return label +} + +// Muted is Label in the muted text color. +func (th *Theme) Muted(text string, scale float32) material.LabelStyle { + label := th.Label(text, scale) + label.Color = th.Colors.MutedText + return label +} + +// ErrorLabel is Label in the error color. +func (th *Theme) ErrorLabel(text string, scale float32) material.LabelStyle { + label := th.Label(text, scale) + label.Color = th.Colors.Error + return label +} + +type UIColors struct { + Background color.NRGBA + SecondaryBackground color.NRGBA + Splitter color.NRGBA + Gutter color.NRGBA + Text color.NRGBA + MutedText color.NRGBA + Error color.NRGBA + Selection color.NRGBA + RelationStroke color.NRGBA +} + +func (c UIColors) SyntaxColors() syntax.Colors { + return syntax.Colors{Text: c.Text, MutedText: c.MutedText, Background: c.Background} +} + +func applyPalette(th *material.Theme, dark bool) UIColors { + if dark { + th.Palette = material.Palette{ + Bg: color.NRGBA{R: 0x11, G: 0x13, B: 0x18, A: 0xff}, + Fg: color.NRGBA{R: 0xe8, G: 0xea, B: 0xed, A: 0xff}, + ContrastBg: color.NRGBA{R: 0x8a, G: 0xb4, B: 0xf8, A: 0xff}, + ContrastFg: color.NRGBA{R: 0x10, G: 0x23, B: 0x3f, A: 0xff}, + } + return UIColors{ + Background: th.Palette.Bg, + SecondaryBackground: color.NRGBA{R: 0x1b, G: 0x1f, B: 0x27, A: 0xff}, + Splitter: color.NRGBA{R: 0x4b, G: 0x55, B: 0x63, A: 0xff}, + Gutter: color.NRGBA{R: 0x18, G: 0x1c, B: 0x22, A: 0xff}, + Text: th.Palette.Fg, + MutedText: color.NRGBA{R: 0xa0, G: 0xa7, B: 0xb5, A: 0xff}, + Error: color.NRGBA{R: 0xff, G: 0xb4, B: 0xab, A: 0xff}, + Selection: color.NRGBA{R: 0x26, G: 0x32, B: 0x47, A: 0xff}, + RelationStroke: color.NRGBA{R: 0xe8, G: 0xea, B: 0xed, A: 0x70}, + } + } + + th.Palette = material.Palette{ + Bg: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + Fg: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, + ContrastBg: color.NRGBA{R: 0x3f, G: 0x51, B: 0xb5, A: 0xff}, + ContrastFg: color.NRGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}, + } + return UIColors{ + Background: th.Palette.Bg, + SecondaryBackground: color.NRGBA{R: 0xf0, G: 0xf0, B: 0xf0, A: 0xff}, + Splitter: color.NRGBA{R: 0x80, G: 0x80, B: 0x80, A: 0xff}, + Gutter: color.NRGBA{R: 0xe8, G: 0xe8, B: 0xe8, A: 0xff}, + Text: th.Palette.Fg, + MutedText: color.NRGBA{R: 0x66, G: 0x66, B: 0x66, A: 0xff}, + Error: color.NRGBA{R: 0xb0, G: 0x00, B: 0x20, A: 0xff}, + Selection: color.NRGBA{R: 0xe8, G: 0xf0, B: 0xfe, A: 0xff}, + RelationStroke: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0x40}, + } +} diff --git a/internal/gui/widgets.go b/internal/gui/widgets.go new file mode 100644 index 0000000..39e97aa --- /dev/null +++ b/internal/gui/widgets.go @@ -0,0 +1,172 @@ +package gui + +import ( + "image" + "image/color" + "time" + + "gioui.org/font" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" + + "loov.dev/lensm/internal/syntax" +) + +// SourceLine is a single-line of text. +type SourceLine struct { + TopLeft image.Point + Width int + Text string + Spans []syntax.Span + TextHeight unit.Sp + Italic bool + Bold bool + Color color.NRGBA +} + +// codeLineHeightScale leaves room for ascenders and descenders when a +// line is clipped to its width; rows themselves advance by the tighter +// CodeUIStyle.LineHeight. +const codeLineHeightScale = 4.0 / 3.0 + +func CodeLineHeightPx(gtx layout.Context, textHeight unit.Sp) int { + height := gtx.Metric.Sp(textHeight * codeLineHeightScale) + if height < 1 { + return 1 + } + return height +} + +// Layout draws the text. +func (line SourceLine) Layout(th *material.Theme, gtx layout.Context) { + gtx.Constraints.Min.X = 0 + gtx.Constraints.Max.X = MaxLineWidth + gtx.Constraints.Min.Y = 0 + gtx.Constraints.Max.Y = MaxLineWidth + + defer op.Offset(line.TopLeft).Push(gtx.Ops).Pop() + if line.Width > 0 { + maxSize := image.Pt(line.Width, CodeLineHeightPx(gtx, line.TextHeight)) + defer clip.Rect{Max: maxSize}.Push(gtx.Ops).Pop() + } + + spans := line.Spans + if len(spans) == 0 { + spans = []syntax.Span{{ + Text: line.Text, + Color: line.Color, + Italic: line.Italic, + Bold: line.Bold, + }} + } + + left := 0 + for _, span := range spans { + if span.Text == "" { + continue + } + dims := layoutSourceSpan(th, gtx, line, span, left) + left += dims.Size.X + } +} + +func layoutSourceSpan(th *material.Theme, gtx layout.Context, line SourceLine, span syntax.Span, left int) layout.Dimensions { + f := font.Font{Typeface: "override-monospace,Go,monospace", Weight: font.Normal} + if line.Italic || span.Italic { + f.Style = font.Italic + } + if line.Bold || span.Bold { + f.Weight = font.Black + } + col := span.Color + if col == (color.NRGBA{}) { + col = line.Color + } + + defer op.Offset(image.Pt(left, 0)).Push(gtx.Ops).Pop() + paint.ColorOp{Color: col}.Add(gtx.Ops) + return widget.Label{MaxLines: 1}.Layout(gtx, th.Shaper, f, line.TextHeight, span.Text, op.CallOp{}) +} + +type VerticalLine struct { + Width unit.Dp + Color color.NRGBA +} + +func (line VerticalLine) Layout(gtx layout.Context) layout.Dimensions { + size := image.Point{ + X: gtx.Metric.Dp(line.Width), + Y: gtx.Constraints.Min.Y, + } + paint.FillShape(gtx.Ops, line.Color, clip.Rect{Max: size}.Op()) + return layout.Dimensions{ + Size: size, + } +} + +type HorizontalLine struct { + Height unit.Dp + Color color.NRGBA +} + +func (line HorizontalLine) Layout(gtx layout.Context) layout.Dimensions { + size := image.Point{ + X: gtx.Constraints.Min.X, + Y: gtx.Metric.Dp(line.Height), + } + paint.FillShape(gtx.Ops, line.Color, clip.Rect{Max: size}.Op()) + return layout.Dimensions{ + Size: size, + } +} + +type ScrollAnimation struct { + active bool + from, to float32 + duration time.Duration + start time.Time +} + +func (anim *ScrollAnimation) Start(gtx layout.Context, from, to float32, duration time.Duration) { + anim.active = true + anim.from = from + anim.to = to + anim.duration = duration + anim.start = gtx.Now + gtx.Execute(op.InvalidateCmd{}) +} + +func (anim *ScrollAnimation) Stop() { anim.active = false } + +func (anim *ScrollAnimation) Update(gtx layout.Context) (float32, bool) { + if !anim.active { + return anim.to, false + } + gtx.Execute(op.InvalidateCmd{}) + + elapsed := gtx.Now.Sub(anim.start) + if elapsed > anim.duration { + anim.active = false + return anim.to, true + } + + progress := float32(elapsed) / float32(anim.duration) + progress = easeInOutCubic(progress) + + pos := anim.from + progress*(anim.to-anim.from) + return pos, true +} + +func easeInOutCubic(t float32) float32 { + if t < .5 { + return 4 * t * t * t + } + return (t-1)*(2*t-2)*(2*t-2) + 1 +} + +const MaxLineWidth = 10 * 1024 diff --git a/internal/gui/windows.go b/internal/gui/windows.go new file mode 100644 index 0000000..f558a14 --- /dev/null +++ b/internal/gui/windows.go @@ -0,0 +1,55 @@ +package gui + +import ( + "fmt" + "image" + "log" + "os" + "sync" + + "gioui.org/app" + "gioui.org/font" + "gioui.org/font/gofont" + "gioui.org/font/opentype" + "gioui.org/unit" +) + +type Windows struct { + active sync.WaitGroup +} + +func (windows *Windows) Open(title string, sizeDp image.Point, run func(*app.Window) error) { + windows.active.Go(func() { + + window := new(app.Window) + window.Option( + app.Title(title), + app.Size(unit.Dp(sizeDp.X), unit.Dp(sizeDp.Y)), + ) + if err := run(window); err != nil { + log.Println(err) + } + }) +} + +func (windows *Windows) Wait() { + windows.active.Wait() +} + +func LoadFonts(userfont string) []font.FontFace { + collection := gofont.Collection() + if userfont == "" { + return collection + } + b, err := os.ReadFile(userfont) + if err != nil { + panic(fmt.Errorf("failed to parse font: %v", err)) + } + face, err := opentype.Parse(b) + if err != nil { + panic(fmt.Errorf("failed to parse font: %v", err)) + } + fnt := font.Font{Typeface: "override-monospace,monospace", Weight: font.Normal} + fface := font.FontFace{Font: fnt, Face: face} + return append(collection, fface) +} diff --git a/internal/mcp/app.go b/internal/mcp/app.go new file mode 100644 index 0000000..9d84b54 --- /dev/null +++ b/internal/mcp/app.go @@ -0,0 +1,272 @@ +package mcp + +import ( + "encoding/json" + "errors" + "fmt" + "io" + "net" + "net/http" + "net/url" + "os" + "strings" + "sync" + "time" + + "loov.dev/lensm/internal/comments" +) + +type AppServer struct { + httpServer *http.Server + url string + load LoadFile + commentsPath string + + // mu guards the fields below. + mu sync.Mutex + session *Session + loadError error + generation uint64 + // active counts in-flight requests using session; a replaced + // session is closed only once they have finished. + active *sync.WaitGroup +} + +func StartAppServer(load LoadFile, commentsPath string) (*AppServer, error) { + listener, err := net.Listen("tcp", "127.0.0.1:7077") + if err != nil { + listener, err = net.Listen("tcp", "127.0.0.1:0") + } + if err != nil { + return nil, err + } + + server := &AppServer{ + url: "http://" + listener.Addr().String() + "/mcp", + load: load, + commentsPath: commentsPath, + active: &sync.WaitGroup{}, + } + mux := http.NewServeMux() + mux.HandleFunc("/", server.handleHTTP) + httpServer := &http.Server{ + Handler: mux, + ReadHeaderTimeout: 5 * time.Second, + ReadTimeout: time.Minute, + WriteTimeout: 5 * time.Minute, + IdleTimeout: 2 * time.Minute, + } + server.httpServer = httpServer + + go func() { + if err := httpServer.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Fprintf(os.Stderr, "lensm MCP server stopped: %v\n", err) + } + }() + return server, nil +} + +func (server *AppServer) URL() string { + if server == nil { + return "" + } + return server.url +} + +func (server *AppServer) SetPath(path string, store *comments.Store) { + if server == nil { + return + } + path = comments.CleanPath(path) + + server.mu.Lock() + server.generation++ + generation := server.generation + commentsPath := server.commentsPath + old, oldActive := server.session, server.active + server.session = nil + server.loadError = nil + server.active = &sync.WaitGroup{} + server.mu.Unlock() + // SetPath runs on the UI event loop: don't block on in-flight requests. + closeSessionWhenIdle(old, oldActive) + + if path == "" { + return + } + + go func() { + session, err := NewSessionWithComments(server.load, path, commentsPath, store) + if err != nil { + fmt.Fprintf(os.Stderr, "unable to load MCP session for %q: %v\n", path, err) + server.replaceSession(generation, nil, err) + return + } + server.replaceSession(generation, session, nil) + }() +} + +func (server *AppServer) Close() error { + if server == nil { + return nil + } + server.mu.Lock() + server.generation++ + old, oldActive := server.session, server.active + server.session = nil + server.loadError = nil + server.active = &sync.WaitGroup{} + server.mu.Unlock() + if old != nil { + if oldActive != nil { + oldActive.Wait() + } + _ = old.Close() + } + if server.httpServer == nil { + return nil + } + return server.httpServer.Close() +} + +func (server *AppServer) replaceSession(generation uint64, session *Session, loadErr error) { + server.mu.Lock() + if generation != server.generation { + server.mu.Unlock() + if session != nil { + _ = session.Close() + } + return + } + old, oldActive := server.session, server.active + server.session = session + server.loadError = loadErr + server.active = &sync.WaitGroup{} + server.mu.Unlock() + closeSessionWhenIdle(old, oldActive) +} + +// closeSessionWhenIdle closes session once in-flight requests holding it +// have finished, without blocking the caller. +func closeSessionWhenIdle(session *Session, active *sync.WaitGroup) { + if session == nil { + return + } + go func() { + if active != nil { + active.Wait() + } + _ = session.Close() + }() +} + +func (server *AppServer) handleHTTP(w http.ResponseWriter, req *http.Request) { + if req.URL.Path != "/" && req.URL.Path != "/mcp" { + http.NotFound(w, req) + return + } + // The MCP streamable-HTTP spec requires validating Origin (and, for a + // loopback server, Host) to block DNS-rebinding pages from driving the + // server; the CORS headers below are advisory only. + if !isLoopbackHost(req.Host) { + http.Error(w, "forbidden host", http.StatusForbidden) + return + } + if origin := req.Header.Get("Origin"); origin != "" && !isLoopbackOrigin(origin) { + http.Error(w, "forbidden origin", http.StatusForbidden) + return + } + w.Header().Set("Access-Control-Allow-Origin", "http://localhost") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Mcp-Session-Id") + + switch req.Method { + case http.MethodOptions: + w.WriteHeader(http.StatusNoContent) + case http.MethodGet: + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "name": "lensm", + "mcp": server.url, + }) + case http.MethodPost: + server.handleHTTPPost(w, req) + default: + http.Error(w, "method not allowed", http.StatusMethodNotAllowed) + } +} + +func (server *AppServer) handleHTTPPost(w http.ResponseWriter, req *http.Request) { + defer req.Body.Close() + data, err := io.ReadAll(io.LimitReader(req.Body, 64*1024*1024)) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + var msg rpcMessage + if err := json.Unmarshal(data, &msg); err != nil { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + _ = json.NewEncoder(w).Encode(rpcMessage{ + JSONRPC: "2.0", + // JSON-RPC 2.0 requires "id": null when the request id is + // unknowable, otherwise strict clients cannot correlate it. + ID: json.RawMessage("null"), + Error: &rpcError{Code: -32700, Message: "parse error"}, + }) + return + } + + response, ok := server.handleHTTPMessage(msg) + if !ok { + w.WriteHeader(http.StatusAccepted) + return + } + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(response) +} + +func (server *AppServer) handleHTTPMessage(msg rpcMessage) (rpcMessage, bool) { + session, loadErr, release := server.acquireSession() + defer release() + return (&mcpServer{session: session, loadErr: loadErr}).handle(msg) +} + +// acquireSession snapshots the current session and keeps it open until +// release is called. The server lock is not held while a request is +// handled, so concurrent requests and SetPath never wait on a handler. +func (server *AppServer) acquireSession() (session *Session, loadErr error, release func()) { + server.mu.Lock() + defer server.mu.Unlock() + if server.active == nil { + server.active = &sync.WaitGroup{} + } + active := server.active + active.Add(1) + return server.session, server.loadError, active.Done +} + +// isLoopbackHost reports whether a Host header (optionally host:port) +// refers to this machine. +func isLoopbackHost(hostport string) bool { + host := hostport + if h, _, err := net.SplitHostPort(hostport); err == nil { + host = h + } + host = strings.Trim(host, "[]") + if strings.EqualFold(host, "localhost") { + return true + } + ip := net.ParseIP(host) + return ip != nil && ip.IsLoopback() +} + +// isLoopbackOrigin reports whether an Origin header value refers to a +// page served from this machine. +func isLoopbackOrigin(origin string) bool { + u, err := url.Parse(origin) + if err != nil { + return false + } + return isLoopbackHost(u.Host) +} diff --git a/internal/mcp/export.go b/internal/mcp/export.go new file mode 100644 index 0000000..e0ecbc0 --- /dev/null +++ b/internal/mcp/export.go @@ -0,0 +1,173 @@ +package mcp + +import ( + "loov.dev/lensm/internal/comments" + "loov.dev/lensm/internal/disasm" +) + +type LineRangeDTO struct { + From int `json:"from"` + To int `json:"to"` +} + +type FunctionCodeDTO struct { + Binary string `json:"binary,omitempty"` + Name string `json:"name"` + File string `json:"file,omitempty"` + Source []SourceFileDTO `json:"source"` + GoAsm []AsmLineDTO `json:"go_asm"` + NativeAsm []AsmLineDTO `json:"native_asm"` + Comments []comments.Record `json:"comments,omitempty"` +} + +type SourceFileDTO struct { + File string `json:"file"` + Blocks []SourceBlockDTO `json:"blocks"` +} + +type SourceBlockDTO struct { + From int `json:"from"` + To int `json:"to"` + Lines []SourceLineDTO `json:"lines"` +} + +type SourceLineDTO struct { + File string `json:"file"` + Line int `json:"line"` + Text string `json:"text"` + Related []LineRangeDTO `json:"related,omitempty"` + Comment string `json:"comment,omitempty"` +} + +type AsmLineDTO struct { + Index int `json:"index"` + PC uint64 `json:"pc"` + PCHex string `json:"pc_hex"` + Text string `json:"text"` + File string `json:"file,omitempty"` + Line int `json:"line,omitempty"` + Call string `json:"call,omitempty"` + RefPC uint64 `json:"ref_pc,omitempty"` + RefPCHex string `json:"ref_pc_hex,omitempty"` + RefOffset int `json:"ref_offset,omitempty"` + Comment string `json:"comment,omitempty"` +} + +func BuildFunctionCodeDTO(binary string, code *disasm.Code, store *comments.Store) FunctionCodeDTO { + if code == nil { + return FunctionCodeDTO{Binary: comments.CleanPath(binary)} + } + + dto := FunctionCodeDTO{ + Binary: comments.CleanPath(binary), + Name: code.Name, + File: code.File, + Comments: store.Filter(code.Name, ""), + } + for _, src := range code.Source { + srcDTO := SourceFileDTO{File: src.File} + for _, block := range src.Blocks { + blockDTO := SourceBlockDTO{ + From: block.From, + To: block.To, + } + for off, text := range block.Lines { + line := block.From + off + lineDTO := SourceLineDTO{ + File: src.File, + Line: line, + Text: text, + } + if off < len(block.Related) { + lineDTO.Related = lineRangesDTO(block.Related[off]) + } + if store != nil { + lineDTO.Comment = store.ForSource(code.Name, src.File, line) + } + blockDTO.Lines = append(blockDTO.Lines, lineDTO) + } + srcDTO.Blocks = append(srcDTO.Blocks, blockDTO) + } + dto.Source = append(dto.Source, srcDTO) + } + + for i, inst := range code.Insts { + goLine := asmLineDTO(i, inst, inst.Text) + nativeLine := asmLineDTO(i, inst, inst.NativeText) + if store != nil && inst.Text != "" { + goLine.Comment = store.ForAsm(code.Name, comments.ViewGoAsm, inst.PC) + } + if store != nil && inst.NativeText != "" { + nativeLine.Comment = store.ForAsm(code.Name, comments.ViewNativeAsm, inst.PC) + } + dto.GoAsm = append(dto.GoAsm, goLine) + dto.NativeAsm = append(dto.NativeAsm, nativeLine) + } + + return dto +} + +func lineRangesDTO(ranges []disasm.LineRange) []LineRangeDTO { + if len(ranges) == 0 { + return nil + } + out := make([]LineRangeDTO, 0, len(ranges)) + for _, r := range ranges { + out = append(out, LineRangeDTO{From: r.From, To: r.To}) + } + return out +} + +func asmLineDTO(index int, inst disasm.Inst, text string) AsmLineDTO { + line := AsmLineDTO{ + Index: index, + PC: inst.PC, + PCHex: comments.FormatPC(inst.PC), + Text: text, + File: inst.File, + Line: inst.Line, + Call: inst.Call, + RefPC: inst.RefPC, + RefOffset: inst.RefOffset, + } + if inst.RefPC != 0 { + line.RefPCHex = comments.FormatPC(inst.RefPC) + } + return line +} + +func sourceLineExists(code *disasm.Code, file string, line int) bool { + if code == nil || file == "" || line <= 0 { + return false + } + for _, src := range code.Source { + if src.File != file && comments.CleanPath(src.File) != comments.CleanPath(file) { + continue + } + for _, block := range src.Blocks { + if block.From <= line && line < block.From+len(block.Lines) { + return true + } + } + } + return false +} + +func asmPCExists(code *disasm.Code, view comments.View, pc uint64) bool { + if code == nil { + return false + } + for _, inst := range code.Insts { + switch view { + case comments.ViewGoAsm: + if inst.PC == pc && inst.Text != "" { + return true + } + case comments.ViewNativeAsm: + if inst.PC == pc && inst.NativeText != "" { + return true + } + } + } + return false +} diff --git a/internal/mcp/mcp.go b/internal/mcp/mcp.go new file mode 100644 index 0000000..0ca0680 --- /dev/null +++ b/internal/mcp/mcp.go @@ -0,0 +1,538 @@ +package mcp + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "flag" + "fmt" + "io" + "os" + "regexp" + "strconv" + "strings" + + "loov.dev/lensm/internal/comments" +) + +const mcpProtocolVersion = "2025-06-18" +const defaultMCPSourceContext = 3 + +// maxMCPSourceContext bounds the per-call context: an unbounded value +// overflows the line-range arithmetic and panics with a slice bounds +// error, letting one request kill the stdio server. +const maxMCPSourceContext = 1000 + +type rpcMessage struct { + JSONRPC string `json:"jsonrpc"` + ID json.RawMessage `json:"id,omitempty"` + Method string `json:"method,omitempty"` + Params json.RawMessage `json:"params,omitempty"` + Result any `json:"result,omitempty"` + Error *rpcError `json:"error,omitempty"` +} + +type rpcError struct { + Code int `json:"code"` + Message string `json:"message"` + Data any `json:"data,omitempty"` +} + +type mcpTool struct { + Name string `json:"name"` + Title string `json:"title,omitempty"` + Description string `json:"description"` + InputSchema any `json:"inputSchema"` +} + +type mcpToolResult struct { + Content []mcpTextContent `json:"content"` + StructuredContent any `json:"structuredContent,omitempty"` + IsError bool `json:"isError,omitempty"` +} + +type mcpTextContent struct { + Type string `json:"type"` + Text string `json:"text"` +} + +type mcpServer struct { + session *Session + loadErr error + out *bufio.Writer +} + +func RunCommand(load LoadFile, args []string) int { + fs := flag.NewFlagSet("lensm mcp", flag.ContinueOnError) + fs.SetOutput(os.Stderr) + commentsPath := fs.String("comments", "", "comments sidecar path") + if err := fs.Parse(args); err != nil { + return 2 + } + if fs.NArg() != 1 { + fmt.Fprintln(os.Stderr, "usage: lensm mcp [-comments path] ") + return 2 + } + + session, err := NewSessionWithComments(load, fs.Arg(0), *commentsPath, nil) + if err != nil { + fmt.Fprintln(os.Stderr, err) + return 1 + } + defer session.Close() + + server := &mcpServer{ + session: session, + out: bufio.NewWriter(os.Stdout), + } + if err := server.serve(os.Stdin); err != nil && !errors.Is(err, io.EOF) { + fmt.Fprintln(os.Stderr, err) + return 1 + } + return 0 +} + +func (server *mcpServer) serve(input io.Reader) error { + scanner := bufio.NewScanner(input) + scanner.Buffer(make([]byte, 0, 64*1024), 64*1024*1024) + for scanner.Scan() { + line := bytes.TrimSpace(scanner.Bytes()) + if len(line) == 0 { + continue + } + var msg rpcMessage + if err := json.Unmarshal(line, &msg); err != nil { + _ = server.writeMessage(rpcMessage{ + JSONRPC: "2.0", + ID: json.RawMessage("null"), + Error: &rpcError{Code: -32700, Message: "parse error"}, + }) + continue + } + response, ok := server.handle(msg) + if ok { + if err := server.writeMessage(response); err != nil { + return err + } + } + } + return scanner.Err() +} + +func (server *mcpServer) writeMessage(msg rpcMessage) error { + msg.JSONRPC = "2.0" + data, err := json.Marshal(msg) + if err != nil { + return err + } + if _, err := server.out.Write(data); err != nil { + return err + } + if err := server.out.WriteByte('\n'); err != nil { + return err + } + return server.out.Flush() +} + +func (server *mcpServer) handle(msg rpcMessage) (rpcMessage, bool) { + if len(msg.ID) == 0 { + return rpcMessage{}, false + } + response := rpcMessage{JSONRPC: "2.0", ID: msg.ID} + result, err := server.handleRequest(msg.Method, msg.Params) + if err != nil { + response.Error = err + return response, true + } + response.Result = result + return response, true +} + +func (server *mcpServer) handleRequest(method string, params json.RawMessage) (any, *rpcError) { + switch method { + case "initialize": + return map[string]any{ + "protocolVersion": mcpProtocolVersion, + "capabilities": map[string]any{ + "tools": map[string]any{ + "listChanged": false, + }, + }, + "serverInfo": map[string]any{ + "name": "lensm", + "title": "lensm", + "version": "dev", + }, + "instructions": "Use tools to inspect Go source, Go assembly, native assembly, and comments for the loaded executable.", + }, nil + case "ping": + return map[string]any{}, nil + case "tools/list": + return map[string]any{"tools": mcpTools()}, nil + case "tools/call": + return server.handleToolCall(params) + default: + return nil, &rpcError{Code: -32601, Message: "method not found"} + } +} + +func (server *mcpServer) handleToolCall(params json.RawMessage) (any, *rpcError) { + var req struct { + Name string `json:"name"` + Arguments json.RawMessage `json:"arguments"` + } + if err := decodeJSON(params, &req); err != nil { + return nil, invalidParams(err) + } + if len(req.Arguments) == 0 { + req.Arguments = []byte("{}") + } + if server.session == nil { + if server.loadErr != nil { + return toolError(server.loadErr), nil + } + return toolError(errors.New("no executable loaded")), nil + } + + var ( + result any + err error + ) + switch req.Name { + case "list_functions": + result, err = server.toolListFunctions(req.Arguments) + case "get_function": + result, err = server.toolGetFunction(req.Arguments) + case "set_comment": + result, err = server.toolSetComment(req.Arguments) + case "get_comments": + result, err = server.toolGetComments(req.Arguments) + default: + return nil, &rpcError{Code: -32602, Message: "unknown tool: " + req.Name} + } + if err != nil { + return toolError(err), nil + } + return toolSuccess(result), nil +} + +func (server *mcpServer) toolListFunctions(args json.RawMessage) (any, error) { + var req struct { + Filter string `json:"filter"` + Limit int `json:"limit"` + Offset int `json:"offset"` + } + if err := decodeJSON(args, &req); err != nil { + return nil, err + } + if req.Limit <= 0 { + req.Limit = 100 + } + if req.Limit > 1000 { + req.Limit = 1000 + } + if req.Offset < 0 { + req.Offset = 0 + } + + var rx *regexp.Regexp + if req.Filter != "" { + var err error + rx, err = regexp.Compile("(?i)" + req.Filter) + if err != nil { + return nil, err + } + } + + type functionInfo struct { + Index int `json:"index"` + Name string `json:"name"` + } + var all []functionInfo + for i, fn := range server.session.Funcs() { + name := fn.Name() + if rx != nil && !rx.MatchString(name) { + continue + } + all = append(all, functionInfo{Index: i, Name: name}) + } + + var page []functionInfo + if req.Offset < len(all) { + page = all[req.Offset:min(req.Offset+req.Limit, len(all))] + } + return map[string]any{ + "binary": server.session.Path, + "functions": page, + "total": len(all), + "offset": req.Offset, + "limit": req.Limit, + }, nil +} + +func (server *mcpServer) toolGetFunction(args json.RawMessage) (any, error) { + var req struct { + Name string `json:"name"` + Context *int `json:"context"` + } + if err := decodeJSON(args, &req); err != nil { + return nil, err + } + if req.Name == "" { + return nil, errors.New("name is required") + } + context := defaultMCPSourceContext + if req.Context != nil { + context = *req.Context + } + if context < 0 { + return nil, errors.New("context must be non-negative") + } + if context > maxMCPSourceContext { + return nil, fmt.Errorf("context must be at most %d", maxMCPSourceContext) + } + code, err := server.session.LoadCode(req.Name, context) + if err != nil { + return nil, err + } + return BuildFunctionCodeDTO(server.session.Path, code, server.session.Comments), nil +} + +func (server *mcpServer) toolSetComment(args json.RawMessage) (any, error) { + var req struct { + Name string `json:"name"` + View comments.View `json:"view"` + File string `json:"file"` + Line int `json:"line"` + PC json.RawMessage `json:"pc"` + Text string `json:"text"` + } + if err := decodeJSON(args, &req); err != nil { + return nil, err + } + if req.Name == "" { + return nil, errors.New("name is required") + } + if server.session.Comments.Path() == "" { + return nil, errors.New("comments sidecar is unavailable; the comment would not be persisted") + } + code, err := server.session.LoadCode(req.Name, 0) + if err != nil { + return nil, err + } + + var coord comments.Coord + switch req.View { + case comments.ViewSource: + if !sourceLineExists(code, req.File, req.Line) { + return nil, fmt.Errorf("source line %s:%d is not present in function %q", req.File, req.Line, req.Name) + } + coord = comments.Coord{ + Function: req.Name, + View: comments.ViewSource, + File: req.File, + Line: req.Line, + } + case comments.ViewGoAsm, comments.ViewNativeAsm: + pc, ok, err := parsePC(req.PC) + if err != nil { + return nil, err + } + if !ok { + return nil, errors.New("pc is required for asm comments") + } + if !asmPCExists(code, req.View, pc) { + return nil, fmt.Errorf("pc %s is not present in %s for function %q", comments.FormatPC(pc), req.View, req.Name) + } + coord = comments.Coord{ + Function: req.Name, + View: req.View, + PC: pc, + } + default: + return nil, fmt.Errorf("unsupported view %q", req.View) + } + + if err := server.session.Comments.Set(coord, req.Text); err != nil { + return nil, err + } + coord = server.session.Comments.Normalize(coord) + return map[string]any{ + "comment": coord, + "deleted": strings.TrimSpace(req.Text) == "", + "path": server.session.Comments.Path(), + }, nil +} + +func (server *mcpServer) toolGetComments(args json.RawMessage) (any, error) { + var req struct { + Name string `json:"name"` + View comments.View `json:"view"` + } + if err := decodeJSON(args, &req); err != nil { + return nil, err + } + if req.View != "" { + switch req.View { + case comments.ViewSource, comments.ViewGoAsm, comments.ViewNativeAsm: + default: + return nil, fmt.Errorf("unsupported view %q", req.View) + } + } + return map[string]any{ + "binary": server.session.Path, + "path": server.session.Comments.Path(), + "comments": server.session.Comments.Filter(req.Name, req.View), + }, nil +} + +func decodeJSON(data []byte, dst any) error { + if len(bytes.TrimSpace(data)) == 0 { + data = []byte("{}") + } + dec := json.NewDecoder(bytes.NewReader(data)) + dec.UseNumber() + return dec.Decode(dst) +} + +func parsePC(data json.RawMessage) (uint64, bool, error) { + data = bytes.TrimSpace(data) + if len(data) == 0 || bytes.Equal(data, []byte("null")) { + return 0, false, nil + } + var text string + if err := json.Unmarshal(data, &text); err == nil { + text = strings.TrimSpace(text) + pc, err := strconv.ParseUint(text, 0, 64) + if err != nil { + return 0, true, fmt.Errorf("invalid pc %q", text) + } + return pc, true, nil + } + var num json.Number + if err := decodeJSON(data, &num); err != nil { + return 0, true, fmt.Errorf("invalid pc") + } + pc, err := strconv.ParseUint(num.String(), 10, 64) + if err != nil { + return 0, true, fmt.Errorf("invalid pc %q", num.String()) + } + return pc, true, nil +} + +func invalidParams(err error) *rpcError { + return &rpcError{Code: -32602, Message: err.Error()} +} + +func toolSuccess(value any) mcpToolResult { + text, err := json.MarshalIndent(value, "", " ") + if err != nil { + text = fmt.Append(nil, value) + } + return mcpToolResult{ + Content: []mcpTextContent{{ + Type: "text", + Text: string(text), + }}, + StructuredContent: value, + } +} + +func toolError(err error) mcpToolResult { + return mcpToolResult{ + Content: []mcpTextContent{{ + Type: "text", + Text: err.Error(), + }}, + IsError: true, + } +} + +func mcpTools() []mcpTool { + return []mcpTool{ + { + Name: "list_functions", + Title: "List Functions", + Description: "List functions in the loaded executable. The optional filter is a case-insensitive Go regexp.", + InputSchema: objectSchema(map[string]any{ + "filter": stringSchema("Optional case-insensitive regexp matched against function names."), + "limit": integerSchema("Maximum number of functions to return. Defaults to 100, capped at 1000."), + "offset": integerSchema("Number of matching functions to skip."), + }, nil), + }, + { + Name: "get_function", + Title: "Get Function Code", + Description: "Return Go source, Go assembly, native assembly, source-to-asm mappings, and comments for a function.", + InputSchema: objectSchema(map[string]any{ + "name": stringSchema("Exact function name."), + "context": integerSchema("Number of extra source lines to include before and after referenced lines. Defaults to 3."), + }, []string{"name"}), + }, + { + Name: "set_comment", + Title: "Set Comment", + Description: "Set or delete a comment for a source line, Go asm instruction, or native asm instruction. Empty text deletes the comment.", + InputSchema: objectSchema(map[string]any{ + "name": stringSchema("Exact function name."), + "view": enumSchema("Code view for the comment.", []string{ + string(comments.ViewSource), + string(comments.ViewGoAsm), + string(comments.ViewNativeAsm), + }), + "file": stringSchema("Source file path. Required for source comments."), + "line": integerSchema("Source line number. Required for source comments."), + "pc": pcSchema(), + "text": stringSchema("Comment text. Empty string deletes the comment."), + }, []string{"name", "view", "text"}), + }, + { + Name: "get_comments", + Title: "Get Comments", + Description: "Return comments, optionally filtered by function name and view.", + InputSchema: objectSchema(map[string]any{ + "name": stringSchema("Optional exact function name."), + "view": enumSchema("Optional code view filter.", []string{ + string(comments.ViewSource), + string(comments.ViewGoAsm), + string(comments.ViewNativeAsm), + }), + }, nil), + }, + } +} + +func objectSchema(properties map[string]any, required []string) map[string]any { + schema := map[string]any{ + "type": "object", + "properties": properties, + "additionalProperties": false, + } + if len(required) > 0 { + schema["required"] = required + } + return schema +} + +func stringSchema(description string) map[string]any { + return map[string]any{"type": "string", "description": description} +} + +func integerSchema(description string) map[string]any { + return map[string]any{"type": "integer", "description": description} +} + +func enumSchema(description string, values []string) map[string]any { + return map[string]any{"type": "string", "description": description, "enum": values} +} + +func pcSchema() map[string]any { + return map[string]any{ + "description": "Instruction program counter for asm comments. Accepts an integer or hex string such as 0x1000.", + "oneOf": []map[string]any{ + {"type": "integer"}, + {"type": "string"}, + }, + } +} diff --git a/internal/mcp/mcp_test.go b/internal/mcp/mcp_test.go new file mode 100644 index 0000000..295c03d --- /dev/null +++ b/internal/mcp/mcp_test.go @@ -0,0 +1,113 @@ +package mcp + +import ( + "encoding/json" + "errors" + "net/http/httptest" + "path/filepath" + "strings" + "testing" + "time" + + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/goobj" +) + +func TestAppMCPServerSetPathClearsStaleSessionOnLoadFailure(t *testing.T) { + server := &AppServer{ + load: func(path string) (disasm.File, error) { return goobj.Load(path) }, + session: &Session{Path: "old"}, + } + + server.SetPath(filepath.Join(t.TempDir(), "missing-binary"), nil) + + server.mu.Lock() + if server.session != nil { + t.Fatal("old MCP session was not cleared immediately") + } + server.mu.Unlock() + + deadline := time.After(2 * time.Second) + tick := time.NewTicker(10 * time.Millisecond) + defer tick.Stop() + for { + server.mu.Lock() + loadErr := server.loadError + session := server.session + server.mu.Unlock() + if loadErr != nil { + if session != nil { + t.Fatal("MCP session was set after load failure") + } + return + } + select { + case <-deadline: + t.Fatal("timed out waiting for MCP load failure") + case <-tick.C: + } + } +} + +func TestMCPHandleHTTPRejectsRebinding(t *testing.T) { + server := &AppServer{} + + cases := []struct { + name string + host string + origin string + status int + }{ + {name: "loopback ip", host: "127.0.0.1:7077", status: 200}, + {name: "localhost", host: "localhost:7077", status: 200}, + {name: "ipv6 loopback", host: "[::1]:7077", status: 200}, + {name: "rebound host", host: "evil.example.com:7077", status: 403}, + {name: "loopback origin", host: "127.0.0.1:7077", origin: "http://localhost:8000", status: 200}, + {name: "cross-site origin", host: "127.0.0.1:7077", origin: "https://evil.example.com", status: 403}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + req := httptest.NewRequest("GET", "http://placeholder/", nil) + req.Host = tc.host + if tc.origin != "" { + req.Header.Set("Origin", tc.origin) + } + rec := httptest.NewRecorder() + server.handleHTTP(rec, req) + if rec.Code != tc.status { + t.Fatalf("status = %d, want %d", rec.Code, tc.status) + } + }) + } +} + +func TestMCPHandleHTTPParseErrorHasNullID(t *testing.T) { + server := &AppServer{} + req := httptest.NewRequest("POST", "http://127.0.0.1:7077/mcp", strings.NewReader("{not json")) + rec := httptest.NewRecorder() + server.handleHTTP(rec, req) + if rec.Code != 400 { + t.Fatalf("status = %d, want 400", rec.Code) + } + if !strings.Contains(rec.Body.String(), `"id":null`) { + t.Fatalf("parse error response missing null id: %s", rec.Body.String()) + } +} + +func TestMCPToolCallReportsLoadError(t *testing.T) { + server := &mcpServer{loadErr: errors.New("load failed")} + result, rpcErr := server.handleToolCall(json.RawMessage(`{"name":"list_functions"}`)) + if rpcErr != nil { + t.Fatal(rpcErr.Message) + } + toolResult, ok := result.(mcpToolResult) + if !ok { + t.Fatalf("result type = %T", result) + } + if !toolResult.IsError { + t.Fatal("tool result is not marked as error") + } + if len(toolResult.Content) != 1 || !strings.Contains(toolResult.Content[0].Text, "load failed") { + t.Fatalf("tool error content = %#v", toolResult.Content) + } +} diff --git a/internal/mcp/session.go b/internal/mcp/session.go new file mode 100644 index 0000000..76c46c4 --- /dev/null +++ b/internal/mcp/session.go @@ -0,0 +1,72 @@ +package mcp + +import ( + "fmt" + + "loov.dev/lensm/internal/comments" + "loov.dev/lensm/internal/disasm" +) + +type Session struct { + Path string + File disasm.File + Comments *comments.Store +} + +// LoadFile opens a binary for disassembly. The caller injects an +// implementation so sessions stay independent of the object-file formats. +type LoadFile func(path string) (disasm.File, error) + +func NewSessionWithComments(load LoadFile, path string, commentsPath string, store *comments.Store) (*Session, error) { + file, err := load(path) + if err != nil { + return nil, err + } + if store == nil { + if commentsPath == "" { + commentsPath = comments.DefaultPath(path) + } + var err error + store, err = comments.Open(commentsPath, path) + if err != nil { + _ = file.Close() + return nil, err + } + } + return &Session{ + Path: comments.CleanPath(path), + File: file, + Comments: store, + }, nil +} + +func (s *Session) Close() error { + if s == nil || s.File == nil { + return nil + } + return s.File.Close() +} + +func (s *Session) Funcs() []disasm.Func { + if s == nil || s.File == nil { + return nil + } + return s.File.Funcs() +} + +func (s *Session) FindFunc(name string) disasm.Func { + for _, fn := range s.Funcs() { + if fn.Name() == name { + return fn + } + } + return nil +} + +func (s *Session) LoadCode(name string, context int) (*disasm.Code, error) { + fn := s.FindFunc(name) + if fn == nil { + return nil, fmt.Errorf("function %q not found", name) + } + return fn.Load(disasm.Options{Context: context}) +} diff --git a/internal/syntax/syntax.go b/internal/syntax/syntax.go new file mode 100644 index 0000000..0bbf8db --- /dev/null +++ b/internal/syntax/syntax.go @@ -0,0 +1,453 @@ +package syntax + +import ( + "go/scanner" + "go/token" + "image/color" + "strconv" + "strings" + "unicode" + "unicode/utf8" +) + +const ( + StyleGoLand = "goland-light" + StyleDarcula = "darcula" + StyleMono = "mono" +) + +type Palette struct { + Plain color.NRGBA + Keyword color.NRGBA + Builtin color.NRGBA + String color.NRGBA + Number color.NRGBA + Comment color.NRGBA + Operator color.NRGBA + Register color.NRGBA + Mnemonic color.NRGBA + Symbol color.NRGBA + LineNumber color.NRGBA + CallTarget color.NRGBA +} + +func NormalizeStyle(style string) string { + switch strings.ToLower(strings.TrimSpace(style)) { + case "", "goland", "goland-light", "jetbrains": + return StyleGoLand + case "darcula", "goland-dark", "jetbrains-dark": + return StyleDarcula + case "mono", "monochrome", "plain": + return StyleMono + default: + return StyleGoLand + } +} + +func StyleLabel(style string) string { + switch NormalizeStyle(style) { + case StyleDarcula: + return "Darcula" + case StyleMono: + return "Mono" + default: + return "GoLand" + } +} + +// Colors are the theme colors the palettes derive from. +type Colors struct { + Text color.NRGBA + MutedText color.NRGBA + Background color.NRGBA +} + +func PaletteFor(style string, colors Colors) Palette { + switch NormalizeStyle(style) { + case StyleDarcula: + if !syntaxBackgroundDark(colors.Background) { + return Palette{ + Plain: color.NRGBA{R: 0x2b, G: 0x2d, B: 0x30, A: 0xff}, + Keyword: color.NRGBA{R: 0xa8, G: 0x55, B: 0x22, A: 0xff}, + Builtin: color.NRGBA{R: 0x3a, G: 0x72, B: 0x80, A: 0xff}, + String: color.NRGBA{R: 0x3c, G: 0x78, B: 0x35, A: 0xff}, + Number: color.NRGBA{R: 0x17, G: 0x50, B: 0xeb, A: 0xff}, + Comment: color.NRGBA{R: 0x7a, G: 0x7a, B: 0x7a, A: 0xff}, + Operator: color.NRGBA{R: 0x2b, G: 0x2d, B: 0x30, A: 0xff}, + Register: color.NRGBA{R: 0x66, G: 0x0e, B: 0x7a, A: 0xff}, + Mnemonic: color.NRGBA{R: 0xa8, G: 0x55, B: 0x22, A: 0xff}, + Symbol: color.NRGBA{R: 0x2b, G: 0x68, B: 0x88, A: 0xff}, + LineNumber: colors.MutedText, + CallTarget: color.NRGBA{R: 0x00, G: 0x00, B: 0xee, A: 0xff}, + } + } + return Palette{ + Plain: color.NRGBA{R: 0xa9, G: 0xb7, B: 0xc6, A: 0xff}, + Keyword: color.NRGBA{R: 0xcc, G: 0x78, B: 0x32, A: 0xff}, + Builtin: color.NRGBA{R: 0x88, G: 0xc0, B: 0xd0, A: 0xff}, + String: color.NRGBA{R: 0x6a, G: 0x87, B: 0x59, A: 0xff}, + Number: color.NRGBA{R: 0x68, G: 0x97, B: 0xbb, A: 0xff}, + Comment: color.NRGBA{R: 0x80, G: 0x80, B: 0x80, A: 0xff}, + Operator: color.NRGBA{R: 0xa9, G: 0xb7, B: 0xc6, A: 0xff}, + Register: color.NRGBA{R: 0x98, G: 0x76, B: 0xaa, A: 0xff}, + Mnemonic: color.NRGBA{R: 0xff, G: 0xc6, B: 0x6d, A: 0xff}, + Symbol: color.NRGBA{R: 0xa5, G: 0xc2, B: 0x61, A: 0xff}, + LineNumber: colors.MutedText, + CallTarget: color.NRGBA{R: 0x62, G: 0x9c, B: 0xf6, A: 0xff}, + } + case StyleMono: + return Palette{ + Plain: colors.Text, + Keyword: colors.Text, + Builtin: colors.Text, + String: colors.Text, + Number: colors.Text, + Comment: colors.MutedText, + Operator: colors.Text, + Register: colors.Text, + Mnemonic: colors.Text, + Symbol: colors.Text, + LineNumber: colors.MutedText, + CallTarget: colors.Text, + } + default: + if syntaxBackgroundDark(colors.Background) { + return Palette{ + Plain: color.NRGBA{R: 0xba, G: 0xbe, B: 0xc7, A: 0xff}, + Keyword: color.NRGBA{R: 0xcf, G: 0x8e, B: 0x6d, A: 0xff}, + Builtin: color.NRGBA{R: 0xbc, G: 0x94, B: 0xf9, A: 0xff}, + String: color.NRGBA{R: 0x6a, G: 0xab, B: 0x73, A: 0xff}, + Number: color.NRGBA{R: 0x2a, G: 0xa1, B: 0xf3, A: 0xff}, + Comment: color.NRGBA{R: 0x7a, G: 0x80, B: 0x87, A: 0xff}, + Operator: color.NRGBA{R: 0xba, G: 0xbe, B: 0xc7, A: 0xff}, + Register: color.NRGBA{R: 0xbc, G: 0x94, B: 0xf9, A: 0xff}, + Mnemonic: color.NRGBA{R: 0xcf, G: 0x8e, B: 0x6d, A: 0xff}, + Symbol: color.NRGBA{R: 0x56, G: 0xa8, B: 0xf5, A: 0xff}, + LineNumber: colors.MutedText, + CallTarget: color.NRGBA{R: 0x56, G: 0xa8, B: 0xf5, A: 0xff}, + } + } + return Palette{ + Plain: color.NRGBA{R: 0x08, G: 0x08, B: 0x08, A: 0xff}, + Keyword: color.NRGBA{R: 0x00, G: 0x00, B: 0x80, A: 0xff}, + Builtin: color.NRGBA{R: 0x87, G: 0x10, B: 0x94, A: 0xff}, + String: color.NRGBA{R: 0x00, G: 0x80, B: 0x00, A: 0xff}, + Number: color.NRGBA{R: 0x17, G: 0x50, B: 0xeb, A: 0xff}, + Comment: color.NRGBA{R: 0x8c, G: 0x8c, B: 0x8c, A: 0xff}, + Operator: color.NRGBA{R: 0x00, G: 0x00, B: 0x00, A: 0xff}, + Register: color.NRGBA{R: 0x66, G: 0x0e, B: 0x7a, A: 0xff}, + Mnemonic: color.NRGBA{R: 0x00, G: 0x00, B: 0x80, A: 0xff}, + Symbol: color.NRGBA{R: 0x87, G: 0x10, B: 0x94, A: 0xff}, + LineNumber: colors.MutedText, + CallTarget: color.NRGBA{R: 0x00, G: 0x00, B: 0xee, A: 0xff}, + } + } +} + +func syntaxBackgroundDark(bg color.NRGBA) bool { + return int(bg.R)*299+int(bg.G)*587+int(bg.B)*114 < 128000 +} + +func HighlightSource(lineNo int, line string, palette Palette) []Span { + spans := []Span{{ + Text: lineNumberPrefix(lineNo), + Color: palette.LineNumber, + }} + return append(spans, HighlightGo(line, palette)...) +} + +func HighlightGo(line string, palette Palette) []Span { + if line == "" { + return nil + } + + var spans []Span + fset := token.NewFileSet() + file := fset.AddFile("", -1, len(line)) + var scan scanner.Scanner + scan.Init(file, []byte(line), nil, scanner.ScanComments) + + cursor := 0 + for { + pos, tok, lit := scan.Scan() + if tok == token.EOF { + break + } + if tok == token.SEMICOLON && lit == "\n" { + continue + } + + start := file.Offset(pos) + if start > len(line) { + break + } + if start < cursor { + continue + } + if start > cursor { + appendSyntaxSpan(&spans, line[cursor:start], palette.Plain, false, false) + } + + text := lit + if text == "" { + text = tok.String() + } + if text == "" { + continue + } + + end := start + len(text) + if end > len(line) { + end = len(line) + text = line[start:end] + } + col, italic := goTokenStyle(tok, text, palette) + appendSyntaxSpan(&spans, text, col, false, italic) + cursor = end + } + if cursor < len(line) { + appendSyntaxSpan(&spans, line[cursor:], palette.Plain, false, false) + } + return spans +} + +func HighlightAsm(line string, callTarget string, palette Palette) []Span { + return highlightAsmLine(line, callTarget, palette) +} + +func lineNumberPrefix(lineNo int) string { + var b strings.Builder + b.WriteString(strconv.Itoa(lineNo)) + for b.Len() < 4 { + b.WriteByte(' ') + } + b.WriteByte(' ') + return b.String() +} + +func goTokenStyle(tok token.Token, text string, palette Palette) (color.NRGBA, bool) { + switch { + case tok.IsKeyword(): + return palette.Keyword, false + case tok == token.COMMENT: + return palette.Comment, true + case tok == token.STRING || tok == token.CHAR: + return palette.String, false + case tok == token.INT || tok == token.FLOAT || tok == token.IMAG: + return palette.Number, false + case tok == token.IDENT && goBuiltinNames[text]: + return palette.Builtin, false + case tok.IsOperator(): + return palette.Operator, false + default: + return palette.Plain, false + } +} + +func highlightAsmLine(line string, callTarget string, palette Palette) []Span { + if line == "" { + return nil + } + + code, comment := splitAsmComment(line) + spans := highlightAsmCode(code, callTarget, palette) + if comment != "" { + appendSyntaxSpan(&spans, comment, palette.Comment, false, true) + } + return spans +} + +func splitAsmComment(line string) (string, string) { + commentAt := len(line) + for _, marker := range []string{"//", ";"} { + if ix := strings.Index(line, marker); ix >= 0 && ix < commentAt { + commentAt = ix + } + } + if commentAt == len(line) { + return line, "" + } + return line[:commentAt], line[commentAt:] +} + +func highlightAsmCode(code string, callTarget string, palette Palette) []Span { + var spans []Span + seenMnemonic := false + for i := 0; i < len(code); { + r, size := runeAt(code, i) + if unicode.IsSpace(r) { + start := i + i += size + for i < len(code) { + r, size = runeAt(code, i) + if !unicode.IsSpace(r) { + break + } + i += size + } + appendSyntaxSpan(&spans, code[start:i], palette.Plain, false, false) + continue + } + if isAsmPunctuation(r) { + appendSyntaxSpan(&spans, code[i:i+size], palette.Operator, false, false) + i += size + continue + } + + start := i + i += size + for i < len(code) { + r, size = runeAt(code, i) + if unicode.IsSpace(r) || isAsmPunctuation(r) { + break + } + i += size + } + word := code[start:i] + col := asmWordColor(word, seenMnemonic, callTarget, palette) + bold := false + if !seenMnemonic && strings.TrimSpace(word) != "" { + seenMnemonic = true + bold = true + } + appendSyntaxSpan(&spans, word, col, bold, false) + } + return spans +} + +func asmWordColor(word string, seenMnemonic bool, callTarget string, palette Palette) color.NRGBA { + trimmed := strings.TrimSpace(word) + trimmed = strings.Trim(trimmed, "[]{}") + upper := strings.ToUpper(strings.TrimPrefix(trimmed, "%")) + + switch { + case trimmed == "": + return palette.Plain + case !seenMnemonic: + return palette.Mnemonic + case callTarget != "" && strings.Contains(trimmed, callTarget): + return palette.CallTarget + case isAsmNumber(trimmed): + return palette.Number + case isAsmRegister(upper): + return palette.Register + case strings.ContainsAny(trimmed, "./:<>@") || strings.Contains(trimmed, "(SB)"): + return palette.Symbol + default: + return palette.Plain + } +} + +func isAsmNumber(word string) bool { + word = strings.TrimPrefix(word, "$") + word = strings.TrimPrefix(word, "#") + word = strings.TrimPrefix(word, "-") + word = strings.TrimPrefix(word, "+") + if word == "" { + return false + } + if strings.HasPrefix(word, "0x") || strings.HasPrefix(word, "0X") { + if len(word) <= 2 { + return false + } + for _, r := range word[2:] { + if !('0' <= r && r <= '9') && !('a' <= r && r <= 'f') && !('A' <= r && r <= 'F') { + return false + } + } + return true + } + for _, r := range word { + if !('0' <= r && r <= '9') { + return false + } + } + return true +} + +func isAsmRegister(word string) bool { + if asmRegisterNames[word] { + return true + } + if len(word) >= 2 { + prefix := word[0] + if prefix == 'R' || prefix == 'X' || prefix == 'W' || prefix == 'V' || prefix == 'Q' || prefix == 'D' || prefix == 'S' || prefix == 'B' { + for _, r := range word[1:] { + if r < '0' || r > '9' { + return false + } + } + return true + } + } + return false +} + +func isAsmPunctuation(r rune) bool { + switch r { + case ',', '(', ')', '[', ']', '{', '}', '+', '-', '*': + return true + default: + return false + } +} + +func appendSyntaxSpan(spans *[]Span, text string, col color.NRGBA, bold, italic bool) { + if text == "" { + return + } + last := len(*spans) - 1 + if last >= 0 { + prev := &(*spans)[last] + if prev.Color == col && prev.Bold == bold && prev.Italic == italic { + prev.Text += text + return + } + } + *spans = append(*spans, Span{ + Text: text, + Color: col, + Bold: bold, + Italic: italic, + }) +} + +func runeAt(s string, i int) (rune, int) { + r := rune(s[i]) + if r < utf8.RuneSelf { + return r, 1 + } + return utf8.DecodeRuneInString(s[i:]) +} + +var goBuiltinNames = map[string]bool{ + "any": true, "append": true, "bool": true, "byte": true, "cap": true, + "clear": true, "close": true, "comparable": true, "complex": true, + "complex64": true, "complex128": true, "copy": true, "delete": true, + "error": true, "false": true, "float32": true, "float64": true, + "imag": true, "int": true, "int8": true, "int16": true, "int32": true, + "int64": true, "iota": true, "len": true, "make": true, "max": true, + "min": true, "new": true, "nil": true, "panic": true, "print": true, + "println": true, "real": true, "recover": true, "rune": true, + "string": true, "true": true, "uint": true, "uint8": true, + "uint16": true, "uint32": true, "uint64": true, "uintptr": true, +} + +var asmRegisterNames = map[string]bool{ + "AH": true, "AL": true, "AX": true, "BH": true, "BL": true, "BP": true, + "BX": true, "CH": true, "CL": true, "CS": true, "CX": true, "DH": true, + "DI": true, "DL": true, "DS": true, "DX": true, "ES": true, "FLAGS": true, + "FP": true, "FS": true, "GS": true, "IP": true, "LR": true, "PC": true, + "SB": true, "SI": true, "SP": true, "TLS": true, "ZR": true, + "EAX": true, "EBP": true, "EBX": true, "ECX": true, "EDI": true, + "EDX": true, "EFLAGS": true, "EIP": true, "ESI": true, "ESP": true, + "RAX": true, "RBP": true, "RBX": true, "RCX": true, "RDI": true, + "RDX": true, "RIP": true, "RSB": true, "RSI": true, "RSP": true, +} + +type Span struct { + Text string + Color color.NRGBA + Italic bool + Bold bool +} diff --git a/internal/syntax/syntax_test.go b/internal/syntax/syntax_test.go new file mode 100644 index 0000000..5f2b1a4 --- /dev/null +++ b/internal/syntax/syntax_test.go @@ -0,0 +1,107 @@ +package syntax + +import ( + "image/color" + "strings" + "testing" +) + +func TestNormalizeSyntaxStyle(t *testing.T) { + if got := NormalizeStyle("jetbrains-dark"); got != StyleDarcula { + t.Fatalf("expected darcula alias, got %q", got) + } + if got := NormalizeStyle("unknown"); got != StyleGoLand { + t.Fatalf("expected unknown style to fall back to goland, got %q", got) + } +} + +func TestHighlightGoLine(t *testing.T) { + palette := testSyntaxPalette() + spans := HighlightGo(`if len(s) > 0 { return "x" // ok }`, palette) + + assertSpanColor(t, spans, "if", palette.Keyword) + assertSpanColor(t, spans, "len", palette.Builtin) + assertSpanColor(t, spans, "0", palette.Number) + assertSpanColor(t, spans, `"x"`, palette.String) + assertSpanStyle(t, spans, "// ok }", palette.Comment, false, true) +} + +func TestHighlightAsmLine(t *testing.T) { + palette := testSyntaxPalette() + spans := HighlightAsm("CALL runtime.morestack_noctxt(SB); tail", "runtime.morestack_noctxt", palette) + + assertSpanStyle(t, spans, "CALL", palette.Mnemonic, true, false) + assertSpanColor(t, spans, "runtime.morestack_noctxt", palette.CallTarget) + assertSpanColor(t, spans, "SB", palette.Register) + assertSpanStyle(t, spans, "; tail", palette.Comment, false, true) +} + +func TestHighlightNativeAsmLine(t *testing.T) { + palette := testSyntaxPalette() + spans := HighlightAsm("MOVQ $0X10, %RAX", "", palette) + + assertSpanStyle(t, spans, "MOVQ", palette.Mnemonic, true, false) + assertSpanColor(t, spans, "$0X10", palette.Number) + assertSpanColor(t, spans, "%RAX", palette.Register) +} + +func testSyntaxPalette() Palette { + return Palette{ + Plain: testColor(1), + Keyword: testColor(2), + Builtin: testColor(3), + String: testColor(4), + Number: testColor(5), + Comment: testColor(6), + Operator: testColor(7), + Register: testColor(8), + Mnemonic: testColor(9), + Symbol: testColor(10), + LineNumber: testColor(11), + CallTarget: testColor(12), + } +} + +func testColor(v uint8) color.NRGBA { + return color.NRGBA{R: v, G: v, B: v, A: 0xff} +} + +func assertSpanColor(t *testing.T, spans []Span, text string, col color.NRGBA) { + t.Helper() + span, ok := findSpan(spans, text) + if !ok { + t.Fatalf("span %q not found in %q", text, joinedSpanText(spans)) + } + if span.Color != col { + t.Fatalf("span %q color = %#v, want %#v", text, span.Color, col) + } +} + +func assertSpanStyle(t *testing.T, spans []Span, text string, col color.NRGBA, bold, italic bool) { + t.Helper() + span, ok := findSpan(spans, text) + if !ok { + t.Fatalf("span %q not found in %q", text, joinedSpanText(spans)) + } + if span.Color != col || span.Bold != bold || span.Italic != italic { + t.Fatalf("span %q style = %#v, bold=%v italic=%v; want %#v, bold=%v italic=%v", + text, span.Color, span.Bold, span.Italic, col, bold, italic) + } +} + +func findSpan(spans []Span, text string) (Span, bool) { + for _, span := range spans { + if span.Text == text { + return span, true + } + } + return Span{}, false +} + +func joinedSpanText(spans []Span) string { + var b strings.Builder + for _, span := range spans { + b.WriteString(span.Text) + } + return b.String() +} diff --git a/internal/wasmobj/file.go b/internal/wasmobj/file.go new file mode 100644 index 0000000..bf7e50c --- /dev/null +++ b/internal/wasmobj/file.go @@ -0,0 +1,140 @@ +package wasmobj + +import ( + "fmt" + "os" + "sort" + "strings" + + "github.com/tetratelabs/wabin/binary" + "github.com/tetratelabs/wabin/wasm" + + "loov.dev/lensm/internal/disasm" +) + +var _ disasm.File = (*File)(nil) +var _ disasm.Func = (*Func)(nil) + +// File contains information about the object file. +type File struct { + module *wasm.Module + + funcs []disasm.Func +} + +func (file *File) Funcs() []disasm.Func { return file.funcs } + +// Func contains information about the executable. +type Func struct { + obj *File + index wasm.Index + name string + code *wasm.Code + sortName string +} + +func (fn *Func) Name() string { return fn.name } + +func (file *File) Close() error { + return nil +} + +func Load(path string) (*File, error) { + obj := &File{} + + data, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + module, err := binary.DecodeModule(data, wasm.CoreFeaturesV2) + if err != nil { + return nil, err + } + obj.module = module + + for i, fnname := range module.NameSection.FunctionNames { + code := module.CodeSection[i] + sym := &Func{ + obj: obj, + index: fnname.Index, + name: fnname.Name, + code: code, + sortName: strings.ToLower(fnname.Name), + } + obj.funcs = append(obj.funcs, sym) + } + + sort.SliceStable(obj.funcs, func(i, k int) bool { + return strings.ToLower(obj.funcs[i].Name()) < strings.ToLower(obj.funcs[k].Name()) + }) + + return obj, nil +} + +func (fn *Func) Load(opts disasm.Options) (*disasm.Code, error) { + return fn.obj.LoadCode(fn, opts), nil +} + +func (file *File) LoadCode(fn *Func, opts disasm.Options) *disasm.Code { + code := &disasm.Code{ + Name: fn.name, + } + + // TODO: https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#binary-instr + + for i, b := range fn.code.Body { + code.Insts = append(code.Insts, disasm.Inst{ + PC: uint64(i), + Text: fmt.Sprintf("BYTE 0x%02x", b), + }) + } + return code +} + +/* +func (file *File) LoadCode(fn *Func, opts disasm.Options) *disasm.Code { + dis, err := wasmdisasm.NewDisassembly(*fn.fn, file.module) + if err != nil { + return &disasm.Code{Name: err.Error()} + } + + code := &disasm.Code{ + Name: fn.fn.Name, + } + + for i, ix := range dis.Code { + code.Insts = append(code.Insts, file.toInstr(dis, i, ix)) + } + + return code +} +func (file *File) toInstr(dis *wasmdisasm.Disassembly, i int, ix wasmdisasm.Instr) disasm.Inst { + inst := disasm.Inst{ + PC: uint64(i), + Text: ix.Op.Name + " " + file.immediatesToString(ix.Immediates), + } + + switch ix.Op.Code { + case operators.Call: + target := ix.Immediates[0].(uint32) + fn := file.module.FunctionIndexSpace[target] + inst.Text = ix.Op.Name + " " + fn.Name + inst.Call = fn.Name + + // TODO: figure out ix.Branches and ix.Block.IfElseIndex (similar) + default: + + } + + return inst +} + +func (file *File) immediatesToString(xs []interface{}) string { + var str strings.Builder + for _, im := range xs { + fmt.Fprintf(&str, " %v", im) + } + return str.String() +} +*/ diff --git a/internal/wasmobj/file_test.go b/internal/wasmobj/file_test.go new file mode 100644 index 0000000..4f00e0d --- /dev/null +++ b/internal/wasmobj/file_test.go @@ -0,0 +1,35 @@ +package wasmobj + +import ( + "path/filepath" + "regexp" + "testing" + + "loov.dev/lensm/internal/disasm" +) + +var rxByteInst = regexp.MustCompile(`^BYTE 0x[0-9a-f]{2}$`) + +func TestLoadFormatsBytes(t *testing.T) { + file, err := Load(filepath.Join("..", "..", "testdata", "c-wasm", "example.wasm")) + if err != nil { + t.Fatal(err) + } + + for _, fn := range file.funcs { + code, err := fn.Load(disasm.Options{}) + if err != nil { + t.Fatal(err) + } + if len(code.Insts) == 0 { + continue + } + for _, inst := range code.Insts { + if !rxByteInst.MatchString(inst.Text) { + t.Fatalf("instruction text = %q", inst.Text) + } + } + return + } + t.Fatal("no wasm function with body found") +} diff --git a/loader.go b/loader.go new file mode 100644 index 0000000..cb33e9a --- /dev/null +++ b/loader.go @@ -0,0 +1,136 @@ +package main + +import ( + "os" + "strings" + "time" + + "loov.dev/lensm/internal/disasm" + "loov.dev/lensm/internal/goobj" + "loov.dev/lensm/internal/wasmobj" +) + +type fileLoadRequest struct { + generation uint64 + path string +} + +type fileLoadResult struct { + generation uint64 + file disasm.File + err error +} + +func loadDisasmFile(path string) (disasm.File, error) { + if workInProgressWASM { + return wasmobj.Load(path) + } + return goobj.Load(path) +} + +// loader loads disassembly files on its own goroutine and, when watch +// is set, polls the current path for modifications with a debounce so +// a binary is not reloaded mid-write. +type loader struct { + loadFile func(string) (disasm.File, error) + watch bool + requests chan fileLoadRequest + results chan fileLoadResult + stop chan struct{} +} + +func newLoader(loadFile func(string) (disasm.File, error), watch bool) *loader { + l := &loader{ + loadFile: loadFile, + watch: watch, + requests: make(chan fileLoadRequest, 1), + results: make(chan fileLoadResult, 1), + stop: make(chan struct{}), + } + go l.run() + return l +} + +func (l *loader) Results() <-chan fileLoadResult { return l.results } + +func (l *loader) Close() { close(l.stop) } + +// Request replaces any queued request; only the latest path matters. +// Main event loop only. +func (l *loader) Request(generation uint64, path string) { + select { + case <-l.requests: + default: + } + l.requests <- fileLoadRequest{generation: generation, path: path} +} + +// finish publishes a result, replacing an unconsumed previous one and +// closing its file. +func (l *loader) finish(result fileLoadResult) { + select { + case old := <-l.results: + if old.file != nil { + _ = old.file.Close() + } + default: + } + l.results <- result +} + +func (l *loader) run() { + var lastModTime time.Time + var pendingModTime time.Time + var pendingSince time.Time + var path string + var generation uint64 + tick := time.NewTicker(150 * time.Millisecond) + defer tick.Stop() + + load := func(force bool, now time.Time) { + if path == "" { + return + } + + stat, err := os.Stat(path) + if err != nil { + l.finish(fileLoadResult{generation: generation, err: err}) + return + } + if !force && stat.ModTime().Equal(lastModTime) { + return + } + if !force { + if !stat.ModTime().Equal(pendingModTime) { + pendingModTime = stat.ModTime() + pendingSince = now + return + } + if now.Sub(pendingSince) < 300*time.Millisecond { + return + } + } + lastModTime = stat.ModTime() + pendingModTime = time.Time{} + + file, err := l.loadFile(path) + l.finish(fileLoadResult{generation: generation, file: file, err: err}) + } + + for { + select { + case req := <-l.requests: + path = strings.TrimSpace(req.path) + generation = req.generation + lastModTime = time.Time{} + pendingModTime = time.Time{} + load(true, time.Now()) + case now := <-tick.C: + if l.watch { + load(false, now) + } + case <-l.stop: + return + } + } +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..896f2e3 --- /dev/null +++ b/main.go @@ -0,0 +1,95 @@ +package main + +import ( + "flag" + "fmt" + "image" + "log" + "os" + "runtime/pprof" + + "gioui.org/app" + "gioui.org/text" + "gioui.org/unit" + "gioui.org/widget/material" + + "loov.dev/lensm/internal/gui" + "loov.dev/lensm/internal/mcp" +) + +func main() { + if len(os.Args) > 1 && os.Args[1] == "mcp" { + workInProgressWASM = os.Getenv("LENSM_EXPERIMENT_WASM") != "" + os.Exit(mcp.RunCommand(loadDisasmFile, os.Args[2:])) + } + + cpuprofile := flag.String("cpuprofile", "", "enable cpu profiling") + defaults := DefaultAppSettings() + textSize := flag.Int("text-size", defaults.TextSize, "default font size") + filter := flag.String("filter", "", "filter the functions by regexp") + watch := flag.Bool("watch", false, "auto reload executable") + context := flag.Int("context", 3, "source line context") + comments := flag.String("comments", "", "comments sidecar path") + font := flag.String("font", "", "user font") + + workInProgressWASM = os.Getenv("LENSM_EXPERIMENT_WASM") != "" + + flag.Parse() + exePath := flag.Arg(0) + explicitTextSize := false + flag.Visit(func(f *flag.Flag) { + switch f.Name { + case "text-size": + explicitTextSize = true + } + }) + + if flag.NArg() > 1 { + fmt.Fprintln(os.Stderr, "lensm [exePath]") + flag.Usage() + os.Exit(2) + } + + windows := &gui.Windows{} + + theme := material.NewTheme() + theme.Shaper = text.NewShaper(text.WithCollection(gui.LoadFonts(*font))) + theme.TextSize = unit.Sp(*textSize) + + ui := NewFileUI(windows, theme) + if !explicitTextSize { + theme.TextSize = unit.Sp(ui.Settings.TextSize) + } + if exePath == "" { + exePath = ui.Settings.LastPath + } + ui.Config = FileUIConfig{ + Path: exePath, + Watch: *watch, + Context: *context, + CommentsPath: *comments, + } + ui.Funcs.SetFilter(*filter) + + windows.Open("lensm", image.Pt(1400, 900), ui.Run) + + go func() { + profile(*cpuprofile, windows.Wait) + os.Exit(0) + }() + + // This starts Gio main. + app.Main() +} + +func profile(cpuprofile string, fn func()) { + if cpuprofile != "" { + f, err := os.Create(cpuprofile) + if err != nil { + log.Fatal(err) + } + pprof.StartCPUProfile(f) + defer pprof.StopCPUProfile() + } + fn() +} diff --git a/navigation.go b/navigation.go new file mode 100644 index 0000000..12e2045 --- /dev/null +++ b/navigation.go @@ -0,0 +1,100 @@ +package main + +const navigationHistoryLimit = 256 + +// NavigationHistory tracks function visits independently from open tabs. +type NavigationHistory struct { + entries []string + index int +} + +func (h *NavigationHistory) Reset() { + h.entries = nil + h.index = -1 +} + +func (h *NavigationHistory) Visit(name string) { + if name == "" { + return + } + if h.index >= 0 && h.index < len(h.entries) && h.entries[h.index] == name { + return + } + if h.index+1 < len(h.entries) { + h.entries = h.entries[:h.index+1] + } + h.entries = append(h.entries, name) + if len(h.entries) > navigationHistoryLimit { + drop := len(h.entries) - navigationHistoryLimit + h.entries = h.entries[drop:] + } + h.index = len(h.entries) - 1 +} + +func (h *NavigationHistory) CanBack() bool { + return h.index > 0 +} + +func (h *NavigationHistory) CanForward() bool { + return h.index >= 0 && h.index+1 < len(h.entries) +} + +func (h *NavigationHistory) Back() (string, bool) { + if !h.CanBack() { + return "", false + } + h.index-- + return h.entries[h.index], true +} + +func (h *NavigationHistory) Forward() (string, bool) { + if !h.CanForward() { + return "", false + } + h.index++ + return h.entries[h.index], true +} + +func (h *NavigationHistory) Current() string { + if h.index < 0 || h.index >= len(h.entries) { + return "" + } + return h.entries[h.index] +} + +func (ui *FileUI) recordNavigation(name string) { + if !ui.navigatingHistory { + ui.Navigation.Visit(name) + } +} + +func (ui *FileUI) navigateBack() { + for ui.Navigation.CanBack() { + name, _ := ui.Navigation.Back() + if ui.navigateHistoryEntry(name) { + return + } + } +} + +func (ui *FileUI) navigateForward() { + for ui.Navigation.CanForward() { + name, _ := ui.Navigation.Forward() + if ui.navigateHistoryEntry(name) { + return + } + } +} + +func (ui *FileUI) navigateHistoryEntry(name string) bool { + fn := ui.findFunc(name) + if fn == nil { + return false + } + ui.navigatingHistory = true + ui.openTab(fn, false) + ui.navigatingHistory = false + ui.copyStatus = "" + ui.invalidateMain() + return true +} diff --git a/navigation_test.go b/navigation_test.go new file mode 100644 index 0000000..42c8724 --- /dev/null +++ b/navigation_test.go @@ -0,0 +1,51 @@ +package main + +import "testing" + +func TestNavigationHistory(t *testing.T) { + var history NavigationHistory + history.Reset() + history.Visit("main.A") + history.Visit("main.B") + history.Visit("main.C") + + if got, ok := history.Back(); !ok || got != "main.B" { + t.Fatalf("Back() = %q, %v", got, ok) + } + if got, ok := history.Back(); !ok || got != "main.A" { + t.Fatalf("Back() = %q, %v", got, ok) + } + if _, ok := history.Back(); ok { + t.Fatal("Back succeeded at beginning") + } + if got, ok := history.Forward(); !ok || got != "main.B" { + t.Fatalf("Forward() = %q, %v", got, ok) + } +} + +func TestNavigationHistoryVisitTruncatesForward(t *testing.T) { + var history NavigationHistory + history.Reset() + history.Visit("A") + history.Visit("B") + history.Visit("C") + _, _ = history.Back() + history.Visit("D") + + if history.CanForward() { + t.Fatal("forward history was not truncated") + } + if got, _ := history.Back(); got != "B" { + t.Fatalf("Back() = %q, want B", got) + } +} + +func TestNavigationHistorySkipsDuplicateVisit(t *testing.T) { + var history NavigationHistory + history.Reset() + history.Visit("A") + history.Visit("A") + if history.CanBack() { + t.Fatal("duplicate visit created a history entry") + } +} diff --git a/packaging/macos/Info.plist b/packaging/macos/Info.plist new file mode 100644 index 0000000..5a55224 --- /dev/null +++ b/packaging/macos/Info.plist @@ -0,0 +1,30 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleDisplayName + Lensm + CFBundleExecutable + lensm + CFBundleIdentifier + dev.loov.lensm + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + Lensm + CFBundlePackageType + APPL + CFBundleShortVersionString + 0.0.0 + CFBundleVersion + 1 + LSApplicationCategoryType + public.app-category.developer-tools + LSMinimumSystemVersion + 12.0 + NSHighResolutionCapable + + + diff --git a/screenshot.gif b/screenshot.gif new file mode 100644 index 0000000..7053e42 Binary files /dev/null and b/screenshot.gif differ diff --git a/scripts/build-macos-dmg.sh b/scripts/build-macos-dmg.sh new file mode 100755 index 0000000..12c8685 --- /dev/null +++ b/scripts/build-macos-dmg.sh @@ -0,0 +1,69 @@ +#!/usr/bin/env bash + +set -euo pipefail + +version="${1:-0.0.0}" +build_number="${2:-1}" +expected_arch="${3:-$(go env GOARCH)}" + +case "$version" in + *[!0-9.]* | "") + echo "version must contain only digits and dots: $version" >&2 + exit 2 + ;; +esac +case "$build_number" in + *[!0-9.]* | "") + echo "build number must contain only digits and dots: $build_number" >&2 + exit 2 + ;; +esac +case "$expected_arch" in + arm64 | amd64) ;; + *) + echo "unsupported macOS architecture: $expected_arch" >&2 + exit 2 + ;; +esac + +actual_arch="$(go env GOARCH)" +if [[ "$actual_arch" != "$expected_arch" ]]; then + echo "runner architecture is $actual_arch, expected $expected_arch" >&2 + exit 2 +fi + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +dist="$root/dist" +work="$(mktemp -d "${TMPDIR:-/tmp}/lensm-dmg.XXXXXX")" +trap 'rm -rf "$work"' EXIT + +app="$work/Lensm.app" +contents="$app/Contents" +mkdir -p "$contents/MacOS" +cp "$root/packaging/macos/Info.plist" "$contents/Info.plist" + +/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $version" "$contents/Info.plist" +/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $build_number" "$contents/Info.plist" +plutil -lint "$contents/Info.plist" + +( + cd "$root" + CGO_ENABLED=1 go build -trimpath -ldflags="-s -w" -o "$contents/MacOS/lensm" . +) + +# --deep is deprecated for signing (macOS 13+); sign nested code first. +codesign --force --sign - "$contents/MacOS/lensm" +codesign --force --sign - "$app" +codesign --verify --deep --strict "$app" + +image="$work/image" +mkdir -p "$image" +cp -R "$app" "$image/Lensm.app" +ln -s /Applications "$image/Applications" + +mkdir -p "$dist" +dmg="$dist/Lensm-${version}-macos-${expected_arch}.dmg" +rm -f "$dmg" +hdiutil create -quiet -volname Lensm -srcfolder "$image" -format UDZO -ov "$dmg" +hdiutil verify -quiet "$dmg" +echo "Created $dmg" diff --git a/settings.go b/settings.go new file mode 100644 index 0000000..5580608 --- /dev/null +++ b/settings.go @@ -0,0 +1,124 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "os" + "path/filepath" + "slices" + + "loov.dev/lensm/internal/atomicfile" + "loov.dev/lensm/internal/comments" + "loov.dev/lensm/internal/syntax" +) + +type AppSettings struct { + SyntaxStyle string `json:"syntax_style"` + Dark bool `json:"dark,omitempty"` + ShowNativeAsm bool `json:"show_native_asm"` + ShowAsmHelp bool `json:"show_asm_help"` + TextSize int `json:"text_size,omitempty"` + SidebarRatio float32 `json:"sidebar_ratio,omitempty"` + LastPath string `json:"last_path,omitempty"` + OpenTabs []string `json:"open_tabs,omitempty"` + ActiveTab string `json:"active_tab,omitempty"` +} + +func DefaultAppSettings() AppSettings { + return AppSettings{ + SyntaxStyle: syntax.StyleGoLand, + ShowNativeAsm: true, + ShowAsmHelp: true, + TextSize: 12, + SidebarRatio: 0.15, + } +} + +func LoadAppSettings() (AppSettings, error) { + settings := DefaultAppSettings() + path, err := appSettingsPath() + if err != nil { + return settings, err + } + + data, err := os.ReadFile(path) + if err != nil { + if errors.Is(err, os.ErrNotExist) { + return settings, nil + } + return settings, err + } + if err := json.Unmarshal(data, &settings); err != nil { + // Move the unreadable file aside: callers proceed with defaults, + // and the next save would otherwise silently replace every + // preference the user had. + backup := path + ".corrupt" + if renameErr := os.Rename(path, backup); renameErr == nil { + return DefaultAppSettings(), fmt.Errorf("decode %s (moved to %s): %w", path, backup, err) + } + return DefaultAppSettings(), fmt.Errorf("decode %s: %w", path, err) + } + settings.SyntaxStyle = syntax.NormalizeStyle(settings.SyntaxStyle) + if settings.TextSize <= 0 { + settings.TextSize = DefaultAppSettings().TextSize + } + if settings.SidebarRatio <= 0 || settings.SidebarRatio >= 1 { + settings.SidebarRatio = DefaultAppSettings().SidebarRatio + } + settings.LastPath = comments.CleanPath(settings.LastPath) + settings.OpenTabs = cleanFuncNames(settings.OpenTabs) + if settings.ActiveTab != "" && !slices.Contains(settings.OpenTabs, settings.ActiveTab) { + settings.ActiveTab = "" + } + return settings, nil +} + +func SaveAppSettings(settings AppSettings) error { + settings.SyntaxStyle = syntax.NormalizeStyle(settings.SyntaxStyle) + if settings.TextSize <= 0 { + settings.TextSize = DefaultAppSettings().TextSize + } + settings.LastPath = comments.CleanPath(settings.LastPath) + settings.OpenTabs = cleanFuncNames(settings.OpenTabs) + if settings.ActiveTab != "" && !slices.Contains(settings.OpenTabs, settings.ActiveTab) { + settings.ActiveTab = "" + } + path, err := appSettingsPath() + if err != nil { + return err + } + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + return err + } + data, err := json.MarshalIndent(settings, "", " ") + if err != nil { + return err + } + data = append(data, '\n') + return atomicfile.Write(path, data, 0o644) +} + +func appSettingsPath() (string, error) { + dir, err := os.UserConfigDir() + if err != nil { + return "", err + } + return filepath.Join(dir, "lensm", "config.json"), nil +} + +func cleanFuncNames(names []string) []string { + if len(names) == 0 { + return nil + } + out := make([]string, 0, len(names)) + seen := map[string]bool{} + for _, name := range names { + if name == "" || seen[name] { + continue + } + seen[name] = true + out = append(out, name) + } + return out +} diff --git a/settings_test.go b/settings_test.go new file mode 100644 index 0000000..c3713e2 --- /dev/null +++ b/settings_test.go @@ -0,0 +1,35 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + + "loov.dev/lensm/internal/atomicfile" +) + +func TestAtomicWriteFileReplacesContentsAndCleansTemporaryFile(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.json") + if err := os.WriteFile(path, []byte("old"), 0o644); err != nil { + t.Fatal(err) + } + + if err := atomicfile.Write(path, []byte("new"), 0o644); err != nil { + t.Fatal(err) + } + data, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if got := string(data); got != "new" { + t.Fatalf("settings contents = %q, want new", got) + } + matches, err := filepath.Glob(filepath.Join(dir, ".lensm-settings-*.tmp")) + if err != nil { + t.Fatal(err) + } + if len(matches) != 0 { + t.Fatalf("temporary settings files remain: %v", matches) + } +} diff --git a/testdata/c-wasm/.gitignore b/testdata/c-wasm/.gitignore new file mode 100644 index 0000000..b8e7818 --- /dev/null +++ b/testdata/c-wasm/.gitignore @@ -0,0 +1,2 @@ +*.s.o +*.s \ No newline at end of file diff --git a/testdata/c-wasm/Makefile b/testdata/c-wasm/Makefile new file mode 100644 index 0000000..740827d --- /dev/null +++ b/testdata/c-wasm/Makefile @@ -0,0 +1,16 @@ +PHONY := llc +llc: + clang --target=wasm32 -emit-llvm -g -c -S example.c + llc -O0 -march=wasm32 \ + --filetype=obj \ + example.s + wasm-ld --no-entry --export-all -o example.wasm example.s.o + +PHONY := clang +clang: + clang -g --target=wasm32 --no-standard-libraries -Wl,--no-entry -o example-clang.wasm example.c + +PHONY := odump-clang +odump: + wasm-objdump -x example-clang.wasm + wasm-objdump -x example.wasm diff --git a/testdata/c-wasm/example-clang.wasm b/testdata/c-wasm/example-clang.wasm new file mode 100755 index 0000000..207eeb7 Binary files /dev/null and b/testdata/c-wasm/example-clang.wasm differ diff --git a/testdata/c-wasm/example.c b/testdata/c-wasm/example.c new file mode 100644 index 0000000..90de3c9 --- /dev/null +++ b/testdata/c-wasm/example.c @@ -0,0 +1,14 @@ +int internal_add(int a, int b) { + if(a > 5) { + return a - b; + } + return a + b; +} + +__attribute__((export_name("add"))) +int add(int a, int b) +{ + int res; + res = internal_add(a, b); + return res; +} \ No newline at end of file diff --git a/testdata/c-wasm/example.wasm b/testdata/c-wasm/example.wasm new file mode 100755 index 0000000..0a9d1f8 Binary files /dev/null and b/testdata/c-wasm/example.wasm differ