Compare commits

...

110 Commits

Author SHA1 Message Date
xgopilot
ae690476ba fix: address code review feedback for size report feature
- Fix error handling priority: check waitErr first, then parseErr, then closeErr
- Optimize O(n²) symbol lookup by checking next symbol first
- Add ELF section constants (SHN_LORESERVE, SHN_ABS, etc.) and use them
- Fix documentation: add missing --elf-output-style=LLVM flag
- Fix documentation: correct field names from pkg.ID to pkg.PkgPath

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-17 08:15:12 +00:00
Li Jie
dc39b84187 fix size report parser for elf 2025-11-17 15:31:34 +08:00
Li Jie
faa5330b69 build: fix llvm-readelf call 2025-11-17 15:11:06 +08:00
xgopilot
8e5b34057e fix: address code review feedback for size report feature
- Add bounds checking for uint64→int conversion to prevent overflow
- Reduce max buffer size from 64MB to 4MB with documented constants
- Add comprehensive comments to symbol-to-size calculation algorithm
- Document moduleNameFromSymbol function and symbol naming conventions

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-17 06:34:23 +00:00
Li Jie
cee22db053 feat: add size level aggregation 2025-11-17 14:09:27 +08:00
xushiwei
b25ae1b4e7 Merge pull request #1384 from luoliwoshang/feature/export-different-names-1378
cl: support //export with different symbol names on embedded targets
2025-11-17 08:10:09 +08:00
xushiwei
a6516de181 Merge pull request #1309 from MeteorsLiu/impl-baremetal-gc
feat: implement baremetal gc
2025-11-17 07:41:31 +08:00
xushiwei
4a268650fc Merge pull request #1400 from cpunion/build/refactor-main-module-generation
build: refactor main module generation and fix stdout/stderr null checks
2025-11-17 07:34:03 +08:00
Li Jie
7e4e53eb1c build: refactor emitStdioNobuf for performance and readability 2025-11-16 20:17:30 +08:00
Li Jie
1473ee98f7 build: add package and function docs 2025-11-16 20:16:16 +08:00
xgopilot
8c7e8b6290 build: apply review feedback on main module generation 2025-11-16 20:16:16 +08:00
xgopilot
2a4b2ef023 build: remove error return from genMainModule
The genMainModule function never returns an error, so simplified
its signature to return only Package.

Updated:
- genMainModule signature: (Package, error) -> Package
- Call site in build.go to not handle error
- Both test cases to remove error checking

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-16 20:16:16 +08:00
xgopilot
7abb468592 fix: separate stdout and stderr null checks in emitStdioNobuf
The original code incorrectly used the stdout null check condition
for both stdout and stderr pointer selection. This caused incorrect
behavior when stderr is null but stdout is not, or vice-versa.

This fix separates the null checks for stdout and stderr into
independent conditions, ensuring each stream is properly selected
based on its own null status.

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-16 20:16:16 +08:00
Li Jie
bf9c6abb23 build: refactor main module generation 2025-11-16 20:16:16 +08:00
xushiwei
2d80951e7d Merge pull request #1396 from goplus/pr-1395
build: fix ldflags rewrites and prep caching
2025-11-15 07:36:31 +08:00
xgopilot
034b05c53c cl: remove Underlying() call to reject string type aliases in rewrites
Type aliases like `type T string` are no longer supported for
-ldflags -X rewrites. Only direct *string types are now allowed.

- Removed Underlying() call from isStringPtrType
- Added TestRewriteIgnoresStringAlias test case

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-14 14:49:30 +00:00
xgopilot
1ba7d1e561 fix: change to isStringPtrType for global string var
- Replace isStringType with isStringPtrType to properly validate that only *string type variables can be rewritten with -ldflags -X
- Remove maxStringTypeDepth constant as it's no longer needed
- Update tests to reflect the new function name and add test case for valid *string type
- Fix compileGlobal to use gbl.Type() for accurate type checking

This addresses the review feedback that Go only allows -X rewrites for the basic string type, not derived types like "type T string".

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-14 14:38:46 +00:00
xgopilot
d17ff2592a build: improve error handling and code quality
- Fix missing error handling in exportObject function
- Add explicit warning for non-string variable rewrites
- Improve documentation for maxRewriteValueLength constant

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: cpunion <8459+cpunion@users.noreply.github.com>
2025-11-14 11:37:09 +00:00
Li Jie
4b26cccc90 cl: cover rewrite guards 2025-11-14 18:29:00 +08:00
Li Jie
3a1d8693e9 rewrite: address review feedback 2025-11-14 17:57:05 +08:00
Li Jie
2a52d422c5 cl: broaden rewrite coverage 2025-11-14 17:48:16 +08:00
Li Jie
b0f5d34b39 cl: add rewrite coverage test 2025-11-14 17:32:52 +08:00
Li Jie
8ba8ec71b5 build: write exports into temp files 2025-11-14 16:58:33 +08:00
Li Jie
1e4616a758 build: don't replace ExportFile 2025-11-14 16:58:33 +08:00
Li Jie
22a43622a0 cl: fix global var rewrite in alt pkg 2025-11-14 16:58:30 +08:00
Haolan
065126e270 feat: make defer tls stub for baremetal 2025-11-14 16:13:43 +08:00
Haolan
552156ff40 fix: adjust gc stats struct 2025-11-14 16:13:43 +08:00
Haolan
36e84196c6 test: add test for gc stats 2025-11-14 16:13:43 +08:00
Haolan
7f1e07755a ci: use llgo test instead
ci: use llgo test instead
2025-11-14 16:13:41 +08:00
Haolan
af27d0475d revert some unnecessary change 2025-11-14 16:13:41 +08:00
Haolan
bb29e8c768 docs: add commets for gc mutex
docs: add commets for tinygo gc
2025-11-14 16:13:38 +08:00
Haolan
eec2c271bd feat: replace println with gcPanic 2025-11-14 16:13:38 +08:00
Haolan
1ed924ed50 fix: add pthread GC support for baremetal
test: fix test logic

chore: format codes
2025-11-14 16:13:34 +08:00
Haolan
c46ca84122 revert disabling stdio buffer 2025-11-14 16:13:34 +08:00
Haolan
b36be05c1e fix: GC() signature 2025-11-14 16:13:34 +08:00
Haolan
66a537ad29 fix: add gc dummy mutex 2025-11-14 16:13:34 +08:00
Haolan
33a00dff1b fix: invalid import and improve tests
refactor: remove initGC

test: add test for GC

fix: invalid import in z_gc

ci: test baremetal GC for coverage

ci: test baremetal GC for coverage
2025-11-14 16:13:03 +08:00
Haolan
e4a69ce413 fix: disable buffers 2025-11-14 16:13:03 +08:00
Haolan
531f69ae6a fix: bdwgc.init() causing archive mode building fail
P

P

P
2025-11-14 16:13:03 +08:00
Haolan
812dfd45c9 feat: implement baremetal GC
fix: pthread gc

fix: xiao-esp32c3 symbol

refactor: use clite memset instead of linking

fix: stack top symbol
2025-11-14 16:12:56 +08:00
Li Jie
e2bb68489d build: override vars in alt pkg 2025-11-14 14:37:39 +08:00
Li Jie
9b76be9e9e support ldflags rewrites for initialized globals 2025-11-14 11:59:13 +08:00
xushiwei
2f65c98eb4 Merge pull request #1348 from cpunion/feature/defer-loop
defer: enable loop lowering
2025-11-13 17:59:26 +08:00
Li Jie
aeb5d82d3e ssa: remove unreachable switch case 2025-11-13 16:15:36 +08:00
Li Jie
317de80b42 test: add nested defer/loop/branch/recover tests and fix mixed branch recover mapping 2025-11-13 16:15:33 +08:00
Li Jie
0455ad4443 ssa: Add test for >64 conditional defers; cover panic at eh.go:252-254 2025-11-13 15:48:50 +08:00
Li Jie
e459ca928b runtime: remove overlays for defer workaround 2025-11-13 15:48:50 +08:00
Li Jie
21fef123d2 di: clean unreachable code 2025-11-13 15:48:50 +08:00
Li Jie
983a189c18 test: cover defer recover and nested loops 2025-11-13 15:48:49 +08:00
Li Jie
cb173f91d0 ssa: rely on runtime thread defer TLS 2025-11-13 15:48:49 +08:00
Li Jie
30bde9f6b5 tls: stub handle for go test 2025-11-13 15:48:49 +08:00
Li Jie
3307860d33 ssa: avoid redundant thread defer update 2025-11-13 15:48:49 +08:00
Li Jie
16709411a0 defer: enable loop lowering 2025-11-13 15:48:46 +08:00
xushiwei
3f8c95cf87 Merge pull request #1334 from goplus/dependabot/go_modules/github.com/goplus/gogen-1.19.5
chore(deps): bump github.com/goplus/gogen from 1.19.3 to 1.19.5
2025-11-13 07:49:29 +08:00
xushiwei
0dbe528e6d Merge pull request #1304 from MeteorsLiu/impl-stacksave
feat: implement `llgo.stackSave`
2025-11-13 07:48:01 +08:00
dependabot[bot]
91f6ad9bfa chore(deps): bump github.com/goplus/gogen from 1.19.3 to 1.19.5
Bumps [github.com/goplus/gogen](https://github.com/goplus/gogen) from 1.19.3 to 1.19.5.
- [Release notes](https://github.com/goplus/gogen/releases)
- [Commits](https://github.com/goplus/gogen/compare/v1.19.3...v1.19.5)

---
updated-dependencies:
- dependency-name: github.com/goplus/gogen
  dependency-version: 1.19.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-11 23:40:43 +00:00
xushiwei
4d1906d722 Merge pull request #1391 from goplus/dependabot/go_modules/github.com/goplus/lib-0.3.1
chore(deps): bump github.com/goplus/lib from 0.3.0 to 0.3.1
2025-11-12 07:38:32 +08:00
dependabot[bot]
ff4a180860 chore(deps): bump github.com/goplus/lib from 0.3.0 to 0.3.1
Bumps [github.com/goplus/lib](https://github.com/goplus/lib) from 0.3.0 to 0.3.1.
- [Release notes](https://github.com/goplus/lib/releases)
- [Commits](https://github.com/goplus/lib/compare/v0.3.0...v0.3.1)

---
updated-dependencies:
- dependency-name: github.com/goplus/lib
  dependency-version: 0.3.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-11-11 23:19:50 +00:00
xushiwei
7ce2733edf Merge pull request #1383 from luoliwoshang/feat/nil/recover
fix(runtime): restore SIGSEGV signal handler for non-wasm platforms
2025-11-12 06:43:02 +08:00
luoliwoshang
e11ae0e21b test: add comprehensive tests and CI for export feature
Add extensive test coverage, demo program, and CI integration for
//export with different names feature:

Unit Tests (cl/builtin_test.go):
- TestHandleExportDiffName: core functionality with 4 scenarios
  * Different names with enableExportRename
  * Same names with enableExportRename
  * Different names with spaces in export directive
  * Matching names without enableExportRename
- TestInitLinknameByDocExportDiffNames: flag behavior verification
  * Export with different names when enabled
  * Export with same name when enabled
  * Normal linkname directives
- TestInitLinkExportDiffNames: edge case handling
  * Symbol not found in decl packages (silent handling)

Demo (_demo/embed/export/):
- Example program demonstrating various export patterns
- Verification script testing both embedded and non-embedded targets
- Documents expected behavior and error cases

CI Integration (.github/workflows/llgo.yml):
- Add export demo to embedded target tests
- Ensure feature works correctly across platforms
- Catch regressions in future changes

The tests verify:
✓ Different names work with -target flag
✓ Same names work in all cases
✓ Different names fail without -target flag
✓ Proper error messages for invalid exports
✓ Silent handling for decl packages

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 18:52:58 +08:00
luoliwoshang
dac3365c73 cl,build: support //export with different names on embedded targets
Add support for using different C symbol names in //export directives
when compiling for embedded targets (enabled via -target flag).

This is for TinyGo compatibility on embedded platforms where hardware
interrupt handlers often require specific naming conventions.

Implementation:
- Add EnableExportRename() to control export rename behavior
- Add isExport parameter to initLink callback for context awareness
- Update initLinknameByDoc() to handle export rename logic:
  * When isExport && enableExportRename: allow different names
  * Otherwise: require name match
- Clean error handling in initLink():
  * export + enableExportRename: silent return (already processed)
  * export + !enableExportRename: panic with clear error message
  * other cases: warning for missing linkname

Example:
  //export LPSPI2_IRQHandler
  func interruptLPSPI2() { ... }

The Go function is named interruptLPSPI2 but exported as
LPSPI2_IRQHandler for the hardware vector table.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 18:52:36 +08:00
luoliwoshang
2363d28d57 feat(runtime): add SIGSEGV signal handler to convert nil pointer dereference to recoverable panic
- Add z_rt_default.go with signal handler for SIGSEGV on non-wasm platforms
- Convert segmentation faults from nil pointer access to Go panic
- Enable recover() to catch nil pointer dereference errors
- Use build tag (!wasm) to maintain wasm platform compatibility
- Remove commented-out signal handling code from z_rt.go

This aligns llgo's behavior with standard Go, where accessing nil pointer
fields triggers a recoverable panic instead of immediate program crash.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-10 15:15:51 +08:00
xushiwei
830e8e7058 Merge pull request #1381 from goplus/xgopilot/claude/document-outll-process-1730340128
docs(CLAUDE.md): document out.ll update process after IR changes
2025-11-10 13:55:01 +08:00
xushiwei
34caf518a1 Merge pull request #1386 from goplus/xgopilot/claude/issue-1385-1762256384
chore: remove deprecated //export LLGoVersion comment
2025-11-10 13:53:02 +08:00
xushiwei
a676ba29db Merge pull request #1387 from luoliwoshang/runtime/iface/func
fix(runtime): segmentation fault when calling interface private methods cross-package
2025-11-10 13:51:42 +08:00
xushiwei
d368cade1c Merge pull request #1388 from luoliwoshang/ci/no-auto-update
ci:avoid auto upgrade before brew install
2025-11-10 13:47:43 +08:00
luoliwoshang
4e374a99ff test:refression test for https://github.com/goplus/llgo/issues/1370
Co-authored-by: xgopilot <noreply@goplus.org>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 21:02:36 +08:00
luoliwoshang
41b403aef7 fix(runtime): segmentation fault when calling interface private methods cross-package
Co-authored-by: xgopilot <noreply@goplus.org>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-06 21:00:37 +08:00
luoliwoshang
cda9d682f2 ci:avoid brew auto upgrade dependant 2025-11-06 19:13:11 +08:00
xgopilot
742bfd95a2 chore: remove deprecated //export LLGoVersion comment
Removes the deprecated //export LLGoVersion comment from internal/env/version.go
as it is no longer needed.

Fixes #1385

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <51194195+luoliwoshang@users.noreply.github.com>
2025-11-04 11:45:10 +00:00
xgopilot
01af858a2e docs(CLAUDE.md): remove redundant test failure mention
Removed the sentence about test failures as requested.
The "Why this matters" section now focuses on the benefits
of the process rather than the negative consequences.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <51194195+luoliwoshang@users.noreply.github.com>
2025-10-31 03:43:06 +00:00
xgopilot
c557aa2af1 docs(CLAUDE.md): remove pro tip line
Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <51194195+luoliwoshang@users.noreply.github.com>
2025-10-31 03:34:08 +00:00
xgopilot
c15c7a05b7 docs(CLAUDE.md): add gentests for batch out.ll updates
Updated the out.ll update workflow to document both tools:
- gentests: batch regeneration of all test files (recommended)
- llgen: individual test directory regeneration (for inspection)

Verified gentests works correctly by testing the regeneration
of all out.ll files. The tool successfully processes all
non-Python test directories.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <51194195+luoliwoshang@users.noreply.github.com>
2025-10-31 03:27:49 +00:00
xgopilot
8f5f36e447 docs(CLAUDE.md): document out.ll update process after IR changes
Add comprehensive documentation explaining:
- What out.ll files are (IR comparison test files)
- When to update them (after modifying IR generation logic)
- How to update them (reinstall llgen, regenerate files)
- Why this matters (test consistency and regression detection)

This documentation will help avoid wasting time on repeatedly updating
test files, as it clarifies the required workflow when making compiler
changes that affect IR generation.

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <51194195+luoliwoshang@users.noreply.github.com>
2025-10-31 02:43:06 +00:00
Haolan
bf6f785988 Merge branch 'main' of https://github.com/goplus/llgo into impl-stacksave 2025-10-30 13:48:13 +08:00
xushiwei
86cafff113 Merge pull request #1375 from goplus/dependabot/github_actions/actions/download-artifact-6
chore(deps): bump actions/download-artifact from 5 to 6
2025-10-27 16:37:07 +08:00
xushiwei
3c88949557 Merge pull request #1376 from goplus/dependabot/github_actions/actions/upload-artifact-5
chore(deps): bump actions/upload-artifact from 4 to 5
2025-10-27 16:36:57 +08:00
dependabot[bot]
533ba9ebd8 chore(deps): bump actions/upload-artifact from 4 to 5
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 5.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 00:17:07 +00:00
dependabot[bot]
f34062166b chore(deps): bump actions/download-artifact from 5 to 6
Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/download-artifact/releases)
- [Commits](https://github.com/actions/download-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/download-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-27 00:17:03 +00:00
xushiwei
c036243b3f Merge pull request #1352 from goplus/xgopilot/claude/issue-1351-1760682793
Remove redundant CompareString from llgo:skip directive
2025-10-27 07:18:05 +08:00
xushiwei
e16fc69ce3 Merge pull request #1349 from goplus/xgopilot/claude/issue-1346-1760499310
fix: set build.Default.Compiler to gc in init function
2025-10-27 07:16:15 +08:00
xushiwei
a2c81327ea Merge pull request #1355 from goplus/feature/reflect-indirect
feat(reflect): add Indirect function
2025-10-27 07:13:54 +08:00
xgopilot
a74ca940e2 feat(reflect): add struct test cases for Indirect function
Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-20 02:29:18 +00:00
xgopilot
0c68ae00c9 refactor: rename reflect-indirect to reflectindirect
Follow naming convention of other demo folders (reflectfunc, gotime, etc.)

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-17 09:19:20 +00:00
xgopilot
8d6d1b76f2 refactor: move reflect.Indirect test to _demo and use panic()
- Moved test from _cmptest/reflect_indirect/ to _demo/go/reflect-indirect/
- Refactored to use panic() for validation instead of fmt.Println
- Added proper assertions for all test cases

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-17 09:06:09 +00:00
xgopilot
e47728b053 feat(reflect): add Indirect function
Implements reflect.Indirect function to support pointer dereferencing.
This function returns the value that a pointer points to, or returns
the value unchanged if it's not a pointer.

Fixes #1354

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-17 08:06:30 +00:00
xgopilot
9b397725da Remove redundant CompareString from llgo:skip directive
The CompareString in the llgo:skip directive is redundant because
the overlay mechanism automatically handles symbol skipping when
an overlay package defines a symbol.

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-17 06:36:46 +00:00
xgopilot
420ad8e010 refactor: simplify go/build patch - use build.Default directly
Remove unnecessary go:linkname, runtime import, and unsafe import.
Access build.Default.Compiler directly instead of via linkname.

- Removed go:linkname directive
- Removed runtime and unsafe imports
- Removed unnecessary _ = runtime.Compiler line
- Simplified from 27 lines to 19 lines
- Same functionality, cleaner code

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 13:28:54 +00:00
xgopilot
6e41cc702f refactor: simplify go/build patch using init function
Use go:linkname to access build.Default and modify Compiler in init function.
This is much simpler than patching defaultContext() and all its dependencies.

- Reduced from 137 lines to 27 lines
- Removed zcgo.go (no longer needed)
- Removed complex parseGoVersion, defaultContext, and helper functions
- Now just sets build.Default.Compiler = "gc" in init()

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 13:09:32 +00:00
xgopilot
0a94a54772 test: enhance demo to test multiple go/build public functions
Enhanced the demo test to cover comprehensive import scenarios:
- Test 1: build.Default context with GOOS/GOARCH validation
- Test 2: Standard library package import (fmt)
- Test 3: Nested standard library package (os/exec)
- Test 4: Internal package import (internal/cpu)
- Test 5: Runtime package with Dir validation
- Test 6: ImportDir with current directory
- Test 7: IsLocalImport with multiple path formats
- Test 8: Context GOPATH/GOROOT validation
- Test 9: Import with AllowBinary flag
- Test 10: Release tags validation

All tests include proper result validation to ensure go/build
patches work correctly across different use cases.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 11:55:02 +00:00
xgopilot
d09ce613c8 fix: remove trailing newline in build.go
Fixed formatting issue that was causing CI to fail.
The file had an extra blank line at the end which go fmt removed.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 11:36:49 +00:00
xgopilot
e614edfab4 test: enhance demo to test multiple go/build public functions
Enhanced the demo test to cover more go/build functionality:
- build.Import (existing)
- build.ImportDir (new)
- build.IsLocalImport (new)
- build.Default context verification (existing, improved)

This ensures the go/build patches work correctly across different
public functions, not just Import.

Also ran go fmt to fix formatting issues reported by CI.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 11:22:19 +00:00
xgopilot
29504f2560 refactor: address final review comments
- Created zcgo.go with defaultCGO_ENABLED constant (Go 1.24 pattern)
- Updated parseGoVersion to panic on unexpected input instead of fallback
- Added runtime.(*Func).Name stub to symtab.go for Go 1.21/1.22 compatibility
- Removed go1.23 build constraint from demo
- Removed internal/buildcfg package (replaced by zcgo.go)

These changes ensure the demo works across Go 1.21+ versions.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 11:07:02 +00:00
xgopilot
ee49fad4a4 refactor: follow Go stdlib conventions for build.go
- Created internal/buildcfg package with DefaultCGO_ENABLED constant
- Updated CGO_ENABLED handling to use buildcfg.DefaultCGO_ENABLED
- Added original Go stdlib comment for release tags
- Simplified toolTags usage by using it directly instead of wrapper function

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 10:39:14 +00:00
xgopilot
946a4bf990 refactor: link internal/buildcfg.ToolTags and enhance demo test coverage
- Link internal/buildcfg.ToolTags variable instead of hardcoding tool tags
- Add comprehensive test cases in demo.go to verify:
  - build.Default.Compiler is correctly patched to "gc"
  - ToolTags are properly populated from internal/buildcfg
  - ReleaseTags are correctly generated
- Validates the go/build patches work as expected

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 08:27:39 +00:00
xgopilot
8b61831b0d refactor: address review comments on build.go
- Remove /llgo from .gitignore (unnecessary)
- Move cgoSupported linkname to top with other linknames
- Fix goVersion initialization timing by making it local variable

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 07:48:28 +00:00
xgopilot
e96625cf07 feat: add go:linkname directives for defaultToolTags and defaultReleaseTags
Add linkname directives and stdlib comments for defaultToolTags and
defaultReleaseTags variables to match Go standard library pattern. This
allows external packages (like gopherjs) to access these internal variables
via linkname, maintaining compatibility with packages that depend on this
behavior.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-16 07:37:13 +00:00
xgopilot
8ac7ada7f9 refactor: use go:linkname to link internal/platform.CgoSupported
Instead of creating a custom platform package, use the go:linkname
directive to directly link to Go's standard library internal/platform.CgoSupported
function. This approach:

- Eliminates the need to maintain a copy of platform support data
- Uses Go's canonical platform information directly
- Reduces code duplication and maintenance burden
- Follows Go's linkname pattern for accessing internal packages

Changes:
- Added import _ "unsafe" to enable linkname
- Added //go:linkname directive for cgoSupported function
- Removed custom runtime/internal/lib/internal/platform package
- Updated function call from platform.CgoSupported to cgoSupported

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 06:05:54 +00:00
xgopilot
0b00e06185 refactor: update platform package to match original Go structure
Updated runtime/internal/lib/internal/platform/platform.go to match
the original Go internal/platform package structure:

- Added OSArch struct with GOOS and GOARCH fields
- Added osArchInfo struct with CgoSupported, FirstClass, and Broken fields
- Created distInfo map using OSArch as keys
- Updated CgoSupported function to use map lookup (matches original implementation)

This change makes the package structure more consistent with Go's
standard library and follows the established pattern more closely.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 05:53:20 +00:00
xgopilot
c4fdb1edc0 refactor: move cgoSupported to internal/platform package
Created runtime/internal/lib/internal/platform package to house the
CgoSupported function, following the established pattern used by other
internal packages like internal/itoa.

Changes:
- Created runtime/internal/lib/internal/platform/platform.go
- Moved cgoSupported function to platform.CgoSupported
- Updated runtime/internal/lib/go/build/build.go to import and use
  the new package

This refactoring improves code organization by separating
platform-specific utilities into their own package, making the code
more maintainable and reusable.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 04:05:58 +00:00
xgopilot
224e3b9440 fix: add Go 1.23+ build constraint to gobuild demo
Add //go:build go1.23 constraint to skip compilation on Go 1.21/1.22
where runtime.(*Func).Name is not implemented in llgo, causing linker
errors when internal/bisect is pulled in as a dependency.

The demo works correctly on Go 1.23+ where the dependency chain or
internal/bisect behavior avoids calling the unimplemented method.

Fixes compatibility issue reported in PR review.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 03:14:32 +00:00
xgopilot
7fbcc8cd10 refactor: use runtime.Version() instead of hardcoded goVersion
Replaced hardcoded goVersion constant with parseGoVersion() function
that dynamically extracts the Go version from runtime.Version().
This eliminates the need to update the version manually.

🤖 Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-16 02:33:53 +00:00
xgopilot
938f883be9 refactor: use hasAltPkg with minimal implementation (94% reduction)
Learned from other AltPkg implementations to create a truly minimal patch:
- Moved from 2073-line overlay to 127-line hasAltPkg implementation
- Replaced internal package dependencies:
  * internal/buildcfg → runtime.GOARCH/GOOS
  * internal/goversion → hardcoded constant (24)
  * internal/platform.CgoSupported → simplified implementation
  * internal/buildcfg.ToolTags → simplified buildToolTags()
- Used type alias (Context = build.Context) to reference stdlib types
- Added go/build to hasAltPkg map
- Removed overlay entirely

This follows the pattern used by other AltPkg packages and achieves
the minimal patching approach requested.

Demo verified working with llgo.

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-15 14:54:29 +00:00
xgopilot
d145967c35 revert: restore full overlay approach for go/build
After investigating the hasAltPkg mechanism, determined that it's not
suitable for go/build.defaultContext() because:

1. hasAltPkg works well for providing additional/alternative functions
2. But defaultContext() needs to REPLACE an existing function that depends
   on internal/buildcfg, internal/goversion, and internal/platform
3. These internal packages cannot be imported from runtime/internal/lib

The full overlay approach (2073 lines) works correctly. Seeking guidance
on whether this is acceptable or if there's an alternative approach.

Demo verified working:
- runtime.Compiler = "llgo"
- go/build.Import() works correctly
- No "unknown compiler" error

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-15 13:11:22 +00:00
xgopilot
2e0fc5fb7f refactor: reduce go/build overlay to minimal 87-line patch
Reduced the go/build overlay from 2073 lines to just 87 lines by only
including the patched defaultContext() function and its direct dependencies
(envOr, defaultGOPATH, and related variables).

The overlay system works by merging with the standard library, so we only
need to provide the functions we're modifying. Unpatched functions
automatically fall back to the Go standard library implementation.

This makes the patch much more maintainable and clearly shows what's
being modified: just the Compiler field assignment in defaultContext().

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-15 12:33:48 +00:00
xgopilot
d8cf93a6cd refactor: move go/build overlay to runtime/internal for minimal patching
- Moved full overlay file from runtime/_overlay/go/build/build.go to
  runtime/internal/go/build/build.go to make it clearer what's being overridden
- Updated runtime/overlay.go to embed from new location
- Created demo under _demo/go/gobuild to demonstrate go/build package working with llgo
- The overlay still contains the full file but is now organized under runtime/internal
  following the project's pattern for patched standard library packages

Fixes #1346

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-15 12:17:38 +00:00
xgopilot
01ada11b74 fix: override go/build.defaultContext() to use gc compiler
Fixes #1346 by creating an overlay for go/build/build.go that sets
Compiler to "gc" in defaultContext(). This allows user code using
go/build package to work with llgo while preserving runtime.Compiler
as "llgo" for identification purposes.

The overlay replaces the entire go/build/build.go file with a modified
version where line 342 changes from 'c.Compiler = runtime.Compiler' to
'c.Compiler = "gc"'. This minimal change ensures go/build recognizes
llgo-compiled binaries without requiring changes to user code.

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <luoliwoshang@users.noreply.github.com>
2025-10-15 11:58:36 +00:00
Haolan
c3dbc580aa chore: update stacksave signature commets 2025-09-28 09:20:50 +08:00
Haolan
93e660a2b0 fix: amd64 getsp demo 2025-09-28 09:20:50 +08:00
Haolan
2ec5653f5e feat: implement llgo.stackSave 2025-09-28 09:20:50 +08:00
92 changed files with 6276 additions and 5953 deletions

View File

@@ -16,6 +16,8 @@ runs:
- name: Install macOS dependencies
if: runner.os == 'macOS'
shell: bash
env:
HOMEBREW_NO_INSTALLED_DEPENDENTS_CHECK: 1
run: |
brew update

View File

@@ -27,7 +27,7 @@ jobs:
wget -P ./_demo/c/llama2-c https://huggingface.co/karpathy/tinyllamas/resolve/main/stories15M.bin
- name: Upload model as artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: llama2-model
path: ./_demo/c/llama2-c/stories15M.bin
@@ -52,7 +52,7 @@ jobs:
with:
llvm-version: ${{matrix.llvm}}
- name: Download model artifact
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: llama2-model
path: ./_demo/c/llama2-c/
@@ -133,6 +133,13 @@ jobs:
chmod +x test.sh
./test.sh
- name: Test export with different symbol names on embedded targets
run: |
echo "Testing //export with different symbol names on embedded targets..."
cd _demo/embed/export
chmod +x verify_export.sh
./verify_export.sh
- name: _xtool build tests
run: |
cd _xtool
@@ -186,11 +193,15 @@ jobs:
uses: actions/setup-go@v6
with:
go-version: ${{matrix.go}}
- name: Test Baremetal GC
if: ${{!startsWith(matrix.os, 'macos')}}
working-directory: runtime/internal/runtime/tinygogc
run: llgo test -tags testGC .
- name: run llgo test
run: |
llgo test ./...
hello:
continue-on-error: true
timeout-minutes: 30

View File

@@ -82,7 +82,7 @@ jobs:
release --verbose --skip=publish,nfpm,snapcraft --snapshot --clean
- name: Upload Darwin AMD64 Artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: llgo-darwin-amd64
path: .dist/*darwin-amd64.tar.gz
@@ -90,7 +90,7 @@ jobs:
include-hidden-files: true
- name: Upload Darwin ARM64 Artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: llgo-darwin-arm64
path: .dist/*darwin-arm64.tar.gz
@@ -98,7 +98,7 @@ jobs:
include-hidden-files: true
- name: Upload Linux AMD64 Artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: llgo-linux-amd64
path: .dist/*linux-amd64.tar.gz
@@ -106,7 +106,7 @@ jobs:
include-hidden-files: true
- name: Upload Linux ARM64 Artifacts
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: llgo-linux-arm64
path: .dist/*linux-arm64.tar.gz
@@ -114,7 +114,7 @@ jobs:
include-hidden-files: true
- name: Upload Checksums
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v5
with:
name: llgo-checksums
path: .dist/*checksums.txt
@@ -158,7 +158,7 @@ jobs:
with:
go-version: ${{ matrix.go-version }}
- name: Download Platform Artifact
uses: actions/download-artifact@v5
uses: actions/download-artifact@v6
with:
name: llgo-${{ matrix.goos }}-${{ matrix.goarch }}
path: .

View File

@@ -51,6 +51,56 @@ go test ./...
**Important:** The `LLGO_ROOT` environment variable must be set to the repository root when running llgo commands during development.
### Update out.ll files after modifying compiler IR generation
**CRITICAL:** When you modify the compiler's IR generation logic (especially in `ssa/` or `cl/` packages), you MUST update all out.ll test files under the `cl/` directory.
#### Understanding out.ll files
The `out.ll` files under the `cl/` directory are comparison IR files that serve as reference outputs for the test suite:
- They are generated by `llgen` from the corresponding `in.go` files in the same directory
- They reflect the current compiler's LLVM IR representation of the Go source code
- They are used by tests to verify that the compiler generates correct and consistent IR output
#### Required steps after modifying IR generation logic
1. **Reinstall the tools** to apply your compiler changes:
```bash
go install -v ./chore/gentests
go install -v ./chore/llgen
```
2. **Regenerate out.ll files**:
**For batch updates (recommended)** - Use `gentests` to regenerate all test files:
```bash
gentests
```
This will automatically regenerate all out.ll files in these directories:
- `cl/_testlibc`
- `cl/_testlibgo`
- `cl/_testrt`
- `cl/_testgo`
- `cl/_testpy`
- `cl/_testdata`
**For individual test inspection** - Use `llgen` to regenerate specific test directories:
```bash
llgen cl/_testgo/interface
llgen cl/_testrt/tpmethod
```
3. **Verify the changes** make sense by reviewing the diff in the out.ll files
4. **Commit the updated out.ll files** along with your compiler changes
#### Why this matters
This process ensures that:
- The test suite reflects the current compiler behavior
- Changes to IR generation are properly documented and reviewed
- Future regressions can be detected by comparing against the reference output
## Code Quality
Before submitting any code updates, you must run the following formatting and validation commands:

View File

@@ -261,7 +261,7 @@ All Go syntax (including `cgo`) is already supported. Here are some examples:
### Defer
LLGo `defer` does not support usage in loops. This is not a bug but a feature, because we think that using `defer` in a loop is a very unrecommended practice.
LLGo now supports `defer` within loops, matching Go's semantics of executing defers in LIFO order for every iteration. The usual caveat from Go still applies: be mindful of loop-heavy defer usage because it allocates per iteration.
### Garbage Collection (GC)

View File

@@ -0,0 +1,22 @@
//go:build amd64
package main
import (
"unsafe"
_ "unsafe"
)
//go:linkname getsp llgo.stackSave
func getsp() unsafe.Pointer
//go:linkname asmFull llgo.asm
func asmFull(instruction string, regs map[string]any) uintptr { return 0 }
func main() {
sp := asmFull("movq %rsp, {}", nil)
if sp != uintptr(getsp()) {
panic("invalid stack pointer")
}
}

View File

@@ -0,0 +1,22 @@
//go:build arm64
package main
import (
"unsafe"
_ "unsafe"
)
//go:linkname getsp llgo.stackSave
func getsp() unsafe.Pointer
//go:linkname asmFull llgo.asm
func asmFull(instruction string, regs map[string]any) uintptr { return 0 }
func main() {
sp := asmFull("mov {}, sp", nil)
if sp != uintptr(getsp()) {
panic("invalid stack pointer")
}
}

View File

@@ -0,0 +1,67 @@
package main
import (
"github.com/goplus/lib/c"
)
// This demo shows how to use //export with different symbol names on embedded targets.
//
// On embedded targets, you can export Go functions with different C symbol names.
// This is useful for hardware interrupt handlers that require specific names.
// Standard Go export - same name
//
//export HelloWorld
func HelloWorld() {
c.Printf(c.Str("Hello from "))
c.Printf(c.Str("HelloWorld\n"))
}
// Embedded target export - different name
// Go function name: interruptLPSPI2
// Exported C symbol: LPSPI2_IRQHandler
//
//export LPSPI2_IRQHandler
func interruptLPSPI2() {
c.Printf(c.Str("LPSPI2 interrupt "))
c.Printf(c.Str("handler called\n"))
}
// Embedded target export - different name
// Go function name: systemTickHandler
// Exported C symbol: SysTick_Handler
//
//export SysTick_Handler
func systemTickHandler() {
c.Printf(c.Str("SysTick "))
c.Printf(c.Str("handler called\n"))
}
// Embedded target export - different name
// Go function name: Add
// Exported C symbol: AddFunc
//
//export AddFunc
func Add(a, b int) int {
result := a + b
c.Printf(c.Str("AddFunc(%d, %d) = %d\n"), a, b, result)
return result
}
func main() {
c.Printf(c.Str("=== Export Demo ===\n\n"))
// Call exported functions directly from Go
c.Printf(c.Str("Calling HelloWorld:\n"))
HelloWorld()
c.Printf(c.Str("\nSimulating hardware interrupts:\n"))
interruptLPSPI2()
systemTickHandler()
c.Printf(c.Str("\nTesting function with return value:\n"))
result := Add(10, 20)
c.Printf(c.Str("Result: %d\n"), result)
c.Printf(c.Str("\n=== Demo Complete ===\n"))
}

View File

@@ -0,0 +1,54 @@
#!/bin/bash
set -e
echo "Building for embedded target..."
# Build for embedded target as executable
# Use llgo directly instead of llgo.sh to avoid go.mod version check
llgo build -o test-verify --target=esp32 .
echo "Checking exported symbols..."
# Get exported symbols
exported_symbols=$(nm -gU ./test-verify.elf | grep -E "(HelloWorld|LPSPI2_IRQHandler|SysTick_Handler|AddFunc)" | awk '{print $NF}')
echo ""
echo "Exported symbols:"
echo "$exported_symbols" | awk '{print " " $0}'
echo ""
# Check expected symbols
expected=("HelloWorld" "LPSPI2_IRQHandler" "SysTick_Handler" "AddFunc")
missing=""
for symbol in "${expected[@]}"; do
if ! echo "$exported_symbols" | grep -q "^$symbol$"; then
missing="$missing $symbol"
fi
done
if [ -n "$missing" ]; then
echo "❌ Missing symbols:$missing"
exit 1
fi
echo "✅ Symbol name mapping verification:"
echo " HelloWorld -> HelloWorld"
echo " interruptLPSPI2 -> LPSPI2_IRQHandler"
echo " systemTickHandler -> SysTick_Handler"
echo " Add -> AddFunc"
echo ""
echo "🎉 All export symbols verified successfully!"
echo ""
echo "Testing that non-embedded target rejects different export names..."
# Build without --target should fail with panic
if llgo build -o test-notarget . 2>&1 | grep -q 'export comment has wrong name "LPSPI2_IRQHandler"'; then
echo "✅ Correctly rejected different export name on non-embedded target"
else
echo "❌ Should have panicked with 'export comment has wrong name' error"
exit 1
fi
echo ""
echo "Note: Different symbol names are only supported on embedded targets."

View File

@@ -297,6 +297,12 @@ github_com_goplus_llgo_runtime_internal_clite_pthread_init(void) GO_SYMBOL_RENAM
void
github_com_goplus_llgo_runtime_internal_clite_pthread_sync_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/pthread/sync.init")
void
github_com_goplus_llgo_runtime_internal_clite_signal_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/signal.init")
void
github_com_goplus_llgo_runtime_internal_clite_tls_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/clite/tls.init")
void
github_com_goplus_llgo_runtime_internal_runtime_goarch_init(void) GO_SYMBOL_RENAME("github.com/goplus/llgo/runtime/internal/runtime/goarch.init")

147
_demo/go/gobuild/demo.go Normal file
View File

@@ -0,0 +1,147 @@
package main
import (
"fmt"
"go/build"
"runtime"
"strings"
)
func main() {
fmt.Printf("runtime.Compiler = %q\n", runtime.Compiler)
// Test 1: Check build.Default context
ctx := build.Default
fmt.Printf("build.Default.Compiler = %q\n", ctx.Compiler)
if ctx.Compiler != "gc" {
panic(fmt.Sprintf("expected build.Default.Compiler to be \"gc\", got %q", ctx.Compiler))
}
if len(ctx.ToolTags) == 0 {
panic("expected build.Default.ToolTags to be non-empty")
}
fmt.Printf("build.Default.ToolTags = %v\n", ctx.ToolTags)
if len(ctx.ReleaseTags) == 0 {
panic("expected build.Default.ReleaseTags to be non-empty")
}
fmt.Printf("build.Default.ReleaseTags count = %d\n", len(ctx.ReleaseTags))
// Validate GOOS and GOARCH are set
if ctx.GOOS == "" {
panic("expected build.Default.GOOS to be non-empty")
}
if ctx.GOARCH == "" {
panic("expected build.Default.GOARCH to be non-empty")
}
fmt.Printf("build.Default.GOOS = %q, GOARCH = %q\n", ctx.GOOS, ctx.GOARCH)
// Test 2: Import standard library package with FindOnly
pkg, err := build.Import("fmt", "", build.FindOnly)
if err != nil {
panic(fmt.Sprintf("build.Import(\"fmt\") failed: %v", err))
}
if pkg.ImportPath != "fmt" {
panic(fmt.Sprintf("expected ImportPath \"fmt\", got %q", pkg.ImportPath))
}
if !pkg.Goroot {
panic("expected fmt package to be in GOROOT")
}
fmt.Printf("build.Import(\"fmt\"): ImportPath=%s, Goroot=%v\n", pkg.ImportPath, pkg.Goroot)
// Test 3: Import nested standard library package
osPkg, err := build.Import("os/exec", "", build.FindOnly)
if err != nil {
panic(fmt.Sprintf("build.Import(\"os/exec\") failed: %v", err))
}
if osPkg.ImportPath != "os/exec" {
panic(fmt.Sprintf("expected ImportPath \"os/exec\", got %q", osPkg.ImportPath))
}
if !osPkg.Goroot {
panic("expected os/exec package to be in GOROOT")
}
fmt.Printf("build.Import(\"os/exec\"): ImportPath=%s, Goroot=%v\n", osPkg.ImportPath, osPkg.Goroot)
// Test 4: Import internal package (should succeed with FindOnly)
internalPkg, err := build.Import("internal/cpu", "", build.FindOnly)
if err != nil {
panic(fmt.Sprintf("build.Import(\"internal/cpu\") failed: %v", err))
}
if internalPkg.ImportPath != "internal/cpu" {
panic(fmt.Sprintf("expected ImportPath \"internal/cpu\", got %q", internalPkg.ImportPath))
}
fmt.Printf("build.Import(\"internal/cpu\"): ImportPath=%s\n", internalPkg.ImportPath)
// Test 5: Import with srcDir parameter
runtimePkg, err := build.Import("runtime", "", build.FindOnly)
if err != nil {
panic(fmt.Sprintf("build.Import(\"runtime\") failed: %v", err))
}
if runtimePkg.ImportPath != "runtime" {
panic(fmt.Sprintf("expected ImportPath \"runtime\", got %q", runtimePkg.ImportPath))
}
if runtimePkg.Dir == "" {
panic("expected runtime package Dir to be non-empty")
}
fmt.Printf("build.Import(\"runtime\"): ImportPath=%s, Dir exists=%v\n", runtimePkg.ImportPath, runtimePkg.Dir != "")
// Test 6: ImportDir with current directory
dirPkg, err := build.ImportDir(".", build.FindOnly)
if err != nil {
panic(fmt.Sprintf("build.ImportDir(\".\") failed: %v", err))
}
// Note: Name might be empty with FindOnly mode as it doesn't read source files
fmt.Printf("build.ImportDir(\".\"): Dir=%s, ImportPath=%s\n", dirPkg.Dir, dirPkg.ImportPath)
// Test 7: IsLocalImport with various paths
testCases := []struct {
path string
expected bool
}{
{"./foo", true},
{"../bar", true},
{"./", true},
{"fmt", false},
{"github.com/user/repo", false},
{"", false},
}
for _, tc := range testCases {
result := build.IsLocalImport(tc.path)
if result != tc.expected {
panic(fmt.Sprintf("build.IsLocalImport(%q): expected %v, got %v", tc.path, tc.expected, result))
}
}
fmt.Printf("build.IsLocalImport: all test cases passed\n")
// Test 8: Verify Context has expected fields
if ctx.GOPATH == "" && ctx.GOROOT == "" {
panic("expected either GOPATH or GOROOT to be set")
}
fmt.Printf("build.Default.GOROOT exists = %v\n", ctx.GOROOT != "")
// Test 9: Import with AllowBinary flag
binaryPkg, err := build.Import("fmt", "", build.FindOnly|build.AllowBinary)
if err != nil {
panic(fmt.Sprintf("build.Import with AllowBinary failed: %v", err))
}
if binaryPkg.ImportPath != "fmt" {
panic(fmt.Sprintf("expected ImportPath \"fmt\", got %q", binaryPkg.ImportPath))
}
fmt.Printf("build.Import(\"fmt\") with AllowBinary: success\n")
// Test 10: Verify compiler tag in build context
hasCompilerTag := false
for _, tag := range ctx.ReleaseTags {
if strings.HasPrefix(tag, "go1.") {
hasCompilerTag = true
break
}
}
if !hasCompilerTag {
panic("expected at least one go1.x release tag")
}
fmt.Printf("build.Default.ReleaseTags: contains go1.x tags = %v\n", hasCompilerTag)
fmt.Printf("\nSuccess! All go/build public functions work correctly with llgo\n")
fmt.Printf("Total tests passed: 10\n")
}

356
_demo/go/gotoken/main.go Normal file
View File

@@ -0,0 +1,356 @@
package main
import (
"fmt"
"go/token"
)
func main() {
testPos()
testToken()
testFileSet()
testFile()
testPosition()
testTokenPrecedence()
testTokenKeywords()
testUtilityFunctions()
}
func testPos() {
fmt.Println("=== Test Pos ===")
pos1 := token.Pos(100)
pos2 := token.Pos(200)
if pos1 != 100 {
panic(fmt.Sprintf("Expected pos1 to be 100, got %d", pos1))
}
if pos2 != 200 {
panic(fmt.Sprintf("Expected pos2 to be 200, got %d", pos2))
}
fmt.Printf("Pos1: %d, Pos2: %d\n", pos1, pos2)
if !pos1.IsValid() {
panic("Expected pos1.IsValid() to be true")
}
fmt.Printf("Pos1.IsValid(): %v\n", pos1.IsValid())
noPos := token.NoPos
if noPos != 0 {
panic(fmt.Sprintf("Expected NoPos to be 0, got %d", noPos))
}
if noPos.IsValid() {
panic("Expected NoPos.IsValid() to be false")
}
fmt.Printf("NoPos: %d, IsValid: %v\n", noPos, noPos.IsValid())
fmt.Println("SUCCESS: Pos operations work correctly\n")
}
func testToken() {
fmt.Println("\n=== Test Token Types ===")
expectedStrings := map[token.Token]string{
token.ADD: "+",
token.SUB: "-",
token.MUL: "*",
token.QUO: "/",
token.LPAREN: "(",
token.RPAREN: ")",
token.EQL: "==",
token.NEQ: "!=",
}
for tok, expected := range expectedStrings {
if tok.String() != expected {
panic(fmt.Sprintf("Expected %v.String() to be %q, got %q", tok, expected, tok.String()))
}
}
tokens := []token.Token{
token.ILLEGAL,
token.EOF,
token.COMMENT,
token.IDENT,
token.INT,
token.FLOAT,
token.IMAG,
token.CHAR,
token.STRING,
token.ADD,
token.SUB,
token.MUL,
token.QUO,
token.REM,
token.AND,
token.OR,
token.XOR,
token.SHL,
token.SHR,
token.AND_NOT,
token.ADD_ASSIGN,
token.SUB_ASSIGN,
token.MUL_ASSIGN,
token.QUO_ASSIGN,
token.REM_ASSIGN,
token.AND_ASSIGN,
token.OR_ASSIGN,
token.XOR_ASSIGN,
token.SHL_ASSIGN,
token.SHR_ASSIGN,
token.AND_NOT_ASSIGN,
token.LAND,
token.LOR,
token.ARROW,
token.INC,
token.DEC,
token.EQL,
token.LSS,
token.GTR,
token.ASSIGN,
token.NOT,
token.NEQ,
token.LEQ,
token.GEQ,
token.DEFINE,
token.ELLIPSIS,
token.LPAREN,
token.LBRACK,
token.LBRACE,
token.COMMA,
token.PERIOD,
token.RPAREN,
token.RBRACK,
token.RBRACE,
token.SEMICOLON,
token.COLON,
}
for _, tok := range tokens {
fmt.Printf("Token: %s (String: %q)\n", tok, tok.String())
}
fmt.Println("SUCCESS: Token types work correctly\n")
}
func testTokenKeywords() {
fmt.Println("\n=== Test Keywords ===")
keywords := []token.Token{
token.BREAK,
token.CASE,
token.CHAN,
token.CONST,
token.CONTINUE,
token.DEFAULT,
token.DEFER,
token.ELSE,
token.FALLTHROUGH,
token.FOR,
token.FUNC,
token.GO,
token.GOTO,
token.IF,
token.IMPORT,
token.INTERFACE,
token.MAP,
token.PACKAGE,
token.RANGE,
token.RETURN,
token.SELECT,
token.STRUCT,
token.SWITCH,
token.TYPE,
token.VAR,
}
for _, kw := range keywords {
if !kw.IsKeyword() {
panic(fmt.Sprintf("Expected %s to be a keyword", kw))
}
fmt.Printf("Keyword: %s, IsKeyword: %v\n", kw, kw.IsKeyword())
}
if token.ADD.IsKeyword() {
panic("Expected ADD operator to not be a keyword")
}
if token.IDENT.IsKeyword() {
panic("Expected IDENT token to not be a keyword")
}
fmt.Println("SUCCESS: Keyword checks work correctly\n")
}
func testTokenPrecedence() {
fmt.Println("\n=== Test Token Precedence ===")
if token.MUL.Precedence() <= token.ADD.Precedence() {
panic("Expected MUL to have higher precedence than ADD")
}
if token.LAND.Precedence() <= token.LOR.Precedence() {
panic("Expected LAND to have higher precedence than LOR")
}
if token.MUL.Precedence() != token.QUO.Precedence() {
panic("Expected MUL and QUO to have same precedence")
}
if token.ADD.Precedence() != token.SUB.Precedence() {
panic("Expected ADD and SUB to have same precedence")
}
operators := []token.Token{
token.ADD,
token.SUB,
token.MUL,
token.QUO,
token.REM,
token.LAND,
token.LOR,
token.EQL,
token.LSS,
token.GTR,
}
for _, op := range operators {
fmt.Printf("Operator: %s, Precedence: %d\n", op, op.Precedence())
}
fmt.Println("SUCCESS: Precedence operations work correctly\n")
}
func testFileSet() {
fmt.Println("\n=== Test FileSet ===")
fset := token.NewFileSet()
file1 := fset.AddFile("file1.go", -1, 1000)
file2 := fset.AddFile("file2.go", -1, 2000)
fmt.Printf("Added file1: %s, Base: %d, Size: %d\n", file1.Name(), file1.Base(), file1.Size())
fmt.Printf("Added file2: %s, Base: %d, Size: %d\n", file2.Name(), file2.Base(), file2.Size())
pos := file1.Pos(100)
retrievedFile := fset.File(pos)
if retrievedFile != file1 {
panic("FileSet.File failed to retrieve correct file")
}
position := fset.Position(pos)
fmt.Printf("Position at offset 100: %s\n", position)
fmt.Println("SUCCESS: FileSet operations work correctly\n")
}
func testFile() {
fmt.Println("\n=== Test File ===")
fset := token.NewFileSet()
file := fset.AddFile("test.go", -1, 1000)
if file.Name() != "test.go" {
panic(fmt.Sprintf("Expected file name to be 'test.go', got %q", file.Name()))
}
if file.Size() != 1000 {
panic(fmt.Sprintf("Expected file size to be 1000, got %d", file.Size()))
}
file.AddLine(0)
file.AddLine(50)
file.AddLine(100)
if file.LineCount() != 3 {
panic(fmt.Sprintf("Expected line count to be 3, got %d", file.LineCount()))
}
fmt.Printf("File name: %s\n", file.Name())
fmt.Printf("File base: %d\n", file.Base())
fmt.Printf("File size: %d\n", file.Size())
fmt.Printf("File line count: %d\n", file.LineCount())
pos := file.Pos(50)
fmt.Printf("Pos at offset 50: %d\n", pos)
offset := file.Offset(pos)
if offset != 50 {
panic(fmt.Sprintf("Expected offset to be 50, got %d", offset))
}
fmt.Printf("Offset of pos: %d\n", offset)
line := file.Line(pos)
if line != 2 {
panic(fmt.Sprintf("Expected line to be 2, got %d", line))
}
fmt.Printf("Line number at pos: %d\n", line)
lineStart := file.LineStart(2)
fmt.Printf("Line 2 starts at pos: %d\n", lineStart)
position := file.Position(pos)
fmt.Printf("Position: %s\n", position)
fmt.Println("SUCCESS: File operations work correctly\n")
}
func testPosition() {
fmt.Println("\n=== Test Position ===" )
pos := token.Position{
Filename: "test.go",
Offset: 100,
Line: 5,
Column: 10,
}
if !pos.IsValid() {
panic("Expected valid position to be valid")
}
if pos.Filename != "test.go" {
panic(fmt.Sprintf("Expected filename to be 'test.go', got %q", pos.Filename))
}
if pos.Line != 5 {
panic(fmt.Sprintf("Expected line to be 5, got %d", pos.Line))
}
if pos.Column != 10 {
panic(fmt.Sprintf("Expected column to be 10, got %d", pos.Column))
}
fmt.Printf("Position: %s\n", pos.String())
fmt.Printf("Filename: %s, Line: %d, Column: %d, Offset: %d\n",
pos.Filename, pos.Line, pos.Column, pos.Offset)
fmt.Printf("IsValid: %v\n", pos.IsValid())
invalidPos := token.Position{}
if invalidPos.IsValid() {
panic("Expected empty position to be invalid")
}
fmt.Printf("Invalid position IsValid: %v\n", invalidPos.IsValid())
fmt.Println("SUCCESS: Position operations work correctly\n")
}
func testUtilityFunctions() {
fmt.Println("\n=== Test Utility Functions ===")
fmt.Printf("IsExported(\"Foo\"): %v\n", token.IsExported("Foo"))
fmt.Printf("IsExported(\"foo\"): %v\n", token.IsExported("foo"))
fmt.Printf("IsExported(\"_foo\"): %v\n", token.IsExported("_foo"))
fmt.Printf("IsIdentifier(\"foo\"): %v\n", token.IsIdentifier("foo"))
fmt.Printf("IsIdentifier(\"foo123\"): %v\n", token.IsIdentifier("foo123"))
fmt.Printf("IsIdentifier(\"123foo\"): %v\n", token.IsIdentifier("123foo"))
fmt.Printf("IsIdentifier(\"foo-bar\"): %v\n", token.IsIdentifier("foo-bar"))
fmt.Printf("IsKeyword(\"func\"): %v\n", token.IsKeyword("func"))
fmt.Printf("IsKeyword(\"if\"): %v\n", token.IsKeyword("if"))
fmt.Printf("IsKeyword(\"foo\"): %v\n", token.IsKeyword("foo"))
lookupFunc := token.Lookup("func")
fmt.Printf("Lookup(\"func\"): %s\n", lookupFunc)
lookupIdent := token.Lookup("myVar")
fmt.Printf("Lookup(\"myVar\"): %s\n", lookupIdent)
lookupFor := token.Lookup("for")
fmt.Printf("Lookup(\"for\"): %s\n", lookupFor)
fmt.Println("SUCCESS: Utility functions work correctly\n")
}

488
_demo/go/gotypes/main.go Normal file
View File

@@ -0,0 +1,488 @@
package main
import (
"fmt"
"go/token"
"go/types"
)
func main() {
testBasicTypes()
testObjects()
testScope()
testPackage()
testNamed()
testInterface()
testStruct()
testSignature()
testTuple()
testArray()
testSlice()
testPointer()
testMap()
testChan()
testTypeComparison()
testTypeChecking()
testStringFunctions()
testLookupFunctions()
testUtilityFunctions()
}
func testBasicTypes() {
fmt.Println("=== Test Basic Types ===")
intType := types.Typ[types.Int]
fmt.Printf("Int type: %v, Kind: %v\n", intType, intType.Kind())
if intType.Kind() != types.Int {
panic(fmt.Sprintf("Int type kind mismatch: expected %v, got %v", types.Int, intType.Kind()))
}
stringType := types.Typ[types.String]
fmt.Printf("String type: %v, Kind: %v\n", stringType, stringType.Kind())
if stringType.Kind() != types.String {
panic(fmt.Sprintf("String type kind mismatch: expected %v, got %v", types.String, stringType.Kind()))
}
boolType := types.Typ[types.Bool]
fmt.Printf("Bool type: %v, Kind: %v\n", boolType, boolType.Kind())
if boolType.Kind() != types.Bool {
panic(fmt.Sprintf("Bool type kind mismatch: expected %v, got %v", types.Bool, boolType.Kind()))
}
float64Type := types.Typ[types.Float64]
fmt.Printf("Float64 type: %v, Kind: %v\n", float64Type, float64Type.Kind())
if float64Type.Kind() != types.Float64 {
panic(fmt.Sprintf("Float64 type kind mismatch: expected %v, got %v", types.Float64, float64Type.Kind()))
}
fmt.Println("SUCCESS: Basic types work correctly\n")
}
func testObjects() {
fmt.Println("\n=== Test Objects (Var, Const, Func, TypeName) ===")
varObj := types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int])
fmt.Printf("Var: Name=%s, Type=%v\n", varObj.Name(), varObj.Type())
if varObj.Name() != "x" {
panic(fmt.Sprintf("Var name mismatch: expected x, got %s", varObj.Name()))
}
if varObj.Type() != types.Typ[types.Int] {
panic(fmt.Sprintf("Var type mismatch: expected int, got %v", varObj.Type()))
}
constObj := types.NewConst(token.NoPos, nil, "pi", types.Typ[types.Float64], nil)
fmt.Printf("Const: Name=%s, Type=%v\n", constObj.Name(), constObj.Type())
if constObj.Name() != "pi" {
panic(fmt.Sprintf("Const name mismatch: expected pi, got %s", constObj.Name()))
}
if constObj.Type() != types.Typ[types.Float64] {
panic(fmt.Sprintf("Const type mismatch: expected float64, got %v", constObj.Type()))
}
sig := types.NewSignatureType(nil, nil, nil, nil, nil, false)
funcObj := types.NewFunc(token.NoPos, nil, "foo", sig)
fmt.Printf("Func: Name=%s, Type=%v\n", funcObj.Name(), funcObj.Type())
if funcObj.Name() != "foo" {
panic(fmt.Sprintf("Func name mismatch: expected foo, got %s", funcObj.Name()))
}
typeObj := types.NewTypeName(token.NoPos, nil, "MyInt", types.Typ[types.Int])
fmt.Printf("TypeName: Name=%s, Type=%v\n", typeObj.Name(), typeObj.Type())
if typeObj.Name() != "MyInt" {
panic(fmt.Sprintf("TypeName name mismatch: expected MyInt, got %s", typeObj.Name()))
}
var obj types.Object = varObj
if obj.Name() != "x" {
panic("Object interface conversion failed")
}
fmt.Println("SUCCESS: Object interface works correctly\n")
}
func testScope() {
fmt.Println("\n=== Test Scope ===")
scope := types.NewScope(nil, 0, 0, "test")
obj := types.NewVar(0, nil, "x", types.Typ[types.Int])
scope.Insert(obj)
result := scope.Lookup("x")
if result != obj {
panic("Scope.Lookup failed")
}
names := scope.Names()
if len(names) != 1 || names[0] != "x" {
panic("Scope.Names failed")
}
num := scope.Len()
if num != 1 {
panic("Scope.Len failed")
}
fmt.Printf("Scope contains %d object(s): %v\n", num, names)
fmt.Println("SUCCESS: Scope operations work correctly\n")
}
func testPackage() {
fmt.Println("\n=== Test Package ===")
pkg := types.NewPackage("example.com/test", "test")
fmt.Printf("Package: Path=%s, Name=%s\n", pkg.Path(), pkg.Name())
if pkg.Path() != "example.com/test" {
panic(fmt.Sprintf("Package path mismatch: expected example.com/test, got %s", pkg.Path()))
}
if pkg.Name() != "test" {
panic(fmt.Sprintf("Package name mismatch: expected test, got %s", pkg.Name()))
}
scope := pkg.Scope()
if scope == nil {
panic("Package.Scope returned nil")
}
varObj := types.NewVar(token.NoPos, pkg, "x", types.Typ[types.Int])
scope.Insert(varObj)
result := pkg.Scope().Lookup("x")
if result != varObj {
panic("Package scope lookup failed")
}
fmt.Println("SUCCESS: Package operations work correctly\n")
}
func testNamed() {
fmt.Println("\n=== Test Named Types ===")
pkg := types.NewPackage("example.com/test", "test")
typeName := types.NewTypeName(token.NoPos, pkg, "MyInt", nil)
named := types.NewNamed(typeName, types.Typ[types.Int], nil)
fmt.Printf("Named type: %v, Underlying: %v\n", named, named.Underlying())
if named.Obj() != typeName {
panic("Named.Obj failed")
}
fmt.Println("SUCCESS: Named type operations work correctly\n")
}
func testInterface() {
fmt.Println("\n=== Test Interface ===")
pkg := types.NewPackage("example.com/test", "test")
posMethod := types.NewFunc(token.NoPos, pkg, "Pos", types.NewSignatureType(nil, nil, nil, nil, types.NewTuple(types.NewVar(0, pkg, "", types.Typ[types.Int])), false))
endMethod := types.NewFunc(token.NoPos, pkg, "End", types.NewSignatureType(nil, nil, nil, nil, types.NewTuple(types.NewVar(0, pkg, "", types.Typ[types.Int])), false))
methods := []*types.Func{posMethod, endMethod}
iface := types.NewInterfaceType(methods, nil)
iface.Complete()
fmt.Printf("Interface with %d methods\n", iface.NumMethods())
if iface.NumMethods() != 2 {
panic(fmt.Sprintf("Interface method count mismatch: expected 2, got %d", iface.NumMethods()))
}
method := iface.Method(0)
fmt.Printf("Method 0: %s\n", method.Name())
if method.Name() != "End" && method.Name() != "Pos" {
panic(fmt.Sprintf("Unexpected method name: %s", method.Name()))
}
fmt.Println("SUCCESS: Interface operations work correctly\n")
}
func testStruct() {
fmt.Println("\n=== Test Struct ===")
fields := []*types.Var{
types.NewField(token.NoPos, nil, "X", types.Typ[types.Int], false),
types.NewField(token.NoPos, nil, "Y", types.Typ[types.String], false),
}
structType := types.NewStruct(fields, nil)
fmt.Printf("Struct with %d fields\n", structType.NumFields())
if structType.NumFields() != 2 {
panic(fmt.Sprintf("Struct field count mismatch: expected 2, got %d", structType.NumFields()))
}
field0 := structType.Field(0)
fmt.Printf("Field 0: Name=%s, Type=%v\n", field0.Name(), field0.Type())
if field0.Name() != "X" {
panic(fmt.Sprintf("Field 0 name mismatch: expected X, got %s", field0.Name()))
}
if field0.Type() != types.Typ[types.Int] {
panic(fmt.Sprintf("Field 0 type mismatch: expected int, got %v", field0.Type()))
}
fmt.Println("SUCCESS: Struct operations work correctly\n")
}
func testSignature() {
fmt.Println("\n=== Test Signature ===")
params := types.NewTuple(
types.NewVar(token.NoPos, nil, "x", types.Typ[types.Int]),
types.NewVar(token.NoPos, nil, "y", types.Typ[types.String]),
)
results := types.NewTuple(
types.NewVar(token.NoPos, nil, "", types.Typ[types.Bool]),
)
sig := types.NewSignatureType(nil, nil, nil, params, results, false)
fmt.Printf("Signature: %d params, %d results\n", sig.Params().Len(), sig.Results().Len())
if sig.Params().Len() != 2 {
panic(fmt.Sprintf("Signature param count mismatch: expected 2, got %d", sig.Params().Len()))
}
if sig.Results().Len() != 1 {
panic(fmt.Sprintf("Signature result count mismatch: expected 1, got %d", sig.Results().Len()))
}
param0 := sig.Params().At(0)
fmt.Printf("Param 0: Name=%s, Type=%v\n", param0.Name(), param0.Type())
if param0.Name() != "x" {
panic(fmt.Sprintf("Param 0 name mismatch: expected x, got %s", param0.Name()))
}
if param0.Type() != types.Typ[types.Int] {
panic(fmt.Sprintf("Param 0 type mismatch: expected int, got %v", param0.Type()))
}
fmt.Println("SUCCESS: Signature operations work correctly\n")
}
func testTuple() {
fmt.Println("\n=== Test Tuple ===")
tuple := types.NewTuple(
types.NewVar(token.NoPos, nil, "a", types.Typ[types.Int]),
types.NewVar(token.NoPos, nil, "b", types.Typ[types.String]),
)
fmt.Printf("Tuple length: %d\n", tuple.Len())
if tuple.Len() != 2 {
panic(fmt.Sprintf("Tuple length mismatch: expected 2, got %d", tuple.Len()))
}
var0 := tuple.At(0)
fmt.Printf("Element 0: Name=%s, Type=%v\n", var0.Name(), var0.Type())
if var0.Name() != "a" {
panic(fmt.Sprintf("Tuple element 0 name mismatch: expected a, got %s", var0.Name()))
}
if var0.Type() != types.Typ[types.Int] {
panic(fmt.Sprintf("Tuple element 0 type mismatch: expected int, got %v", var0.Type()))
}
fmt.Println("SUCCESS: Tuple operations work correctly\n")
}
func testArray() {
fmt.Println("\n=== Test Array ===")
arrayType := types.NewArray(types.Typ[types.Int], 10)
fmt.Printf("Array type: %v, Elem: %v, Len: %d\n", arrayType, arrayType.Elem(), arrayType.Len())
if arrayType.Len() != 10 {
panic(fmt.Sprintf("Array length mismatch: expected 10, got %d", arrayType.Len()))
}
if arrayType.Elem() != types.Typ[types.Int] {
panic(fmt.Sprintf("Array element type mismatch: expected int, got %v", arrayType.Elem()))
}
fmt.Println("SUCCESS: Array operations work correctly\n")
}
func testSlice() {
fmt.Println("\n=== Test Slice ===")
sliceType := types.NewSlice(types.Typ[types.String])
fmt.Printf("Slice type: %v, Elem: %v\n", sliceType, sliceType.Elem())
if sliceType.Elem() != types.Typ[types.String] {
panic(fmt.Sprintf("Slice element type mismatch: expected string, got %v", sliceType.Elem()))
}
fmt.Println("SUCCESS: Slice operations work correctly\n")
}
func testPointer() {
fmt.Println("\n=== Test Pointer ===")
ptrType := types.NewPointer(types.Typ[types.Int])
fmt.Printf("Pointer type: %v, Elem: %v\n", ptrType, ptrType.Elem())
if ptrType.Elem() != types.Typ[types.Int] {
panic(fmt.Sprintf("Pointer element type mismatch: expected int, got %v", ptrType.Elem()))
}
fmt.Println("SUCCESS: Pointer operations work correctly\n")
}
func testMap() {
fmt.Println("\n=== Test Map ===")
mapType := types.NewMap(types.Typ[types.String], types.Typ[types.Int])
fmt.Printf("Map type: %v, Key: %v, Elem: %v\n", mapType, mapType.Key(), mapType.Elem())
if mapType.Key() != types.Typ[types.String] {
panic(fmt.Sprintf("Map key type mismatch: expected string, got %v", mapType.Key()))
}
if mapType.Elem() != types.Typ[types.Int] {
panic(fmt.Sprintf("Map element type mismatch: expected int, got %v", mapType.Elem()))
}
fmt.Println("SUCCESS: Map operations work correctly\n")
}
func testChan() {
fmt.Println("\n=== Test Chan ===" )
chanType := types.NewChan(types.SendRecv, types.Typ[types.Int])
fmt.Printf("Chan type: %v, Dir: %v, Elem: %v\n", chanType, chanType.Dir(), chanType.Elem())
if chanType.Dir() != types.SendRecv {
panic(fmt.Sprintf("Chan direction mismatch: expected SendRecv, got %v", chanType.Dir()))
}
if chanType.Elem() != types.Typ[types.Int] {
panic(fmt.Sprintf("Chan element type mismatch: expected int, got %v", chanType.Elem()))
}
sendChan := types.NewChan(types.SendOnly, types.Typ[types.String])
fmt.Printf("SendOnly chan: %v, Dir: %v\n", sendChan, sendChan.Dir())
if sendChan.Dir() != types.SendOnly {
panic(fmt.Sprintf("SendOnly chan direction mismatch: expected SendOnly, got %v", sendChan.Dir()))
}
recvChan := types.NewChan(types.RecvOnly, types.Typ[types.Bool])
fmt.Printf("RecvOnly chan: %v, Dir: %v\n", recvChan, recvChan.Dir())
if recvChan.Dir() != types.RecvOnly {
panic(fmt.Sprintf("RecvOnly chan direction mismatch: expected RecvOnly, got %v", recvChan.Dir()))
}
fmt.Println("SUCCESS: Chan operations work correctly\n")
}
func testTypeComparison() {
fmt.Println("\n=== Test Type Comparison Functions ===")
t1 := types.Typ[types.Int]
t2 := types.Typ[types.Int]
t3 := types.Typ[types.String]
if !types.Identical(t1, t2) {
panic("Identical failed: int should be identical to int")
}
fmt.Printf("Identical(int, int): %v\n", types.Identical(t1, t2))
fmt.Printf("Identical(int, string): %v\n", types.Identical(t1, t3))
if !types.AssignableTo(t1, t2) {
panic("AssignableTo failed")
}
fmt.Printf("AssignableTo(int, int): %v\n", types.AssignableTo(t1, t2))
fmt.Printf("AssignableTo(int, string): %v\n", types.AssignableTo(t1, t3))
fmt.Printf("Comparable(int): %v\n", types.Comparable(t1))
fmt.Printf("Comparable(string): %v\n", types.Comparable(t3))
fmt.Printf("ConvertibleTo(int, int): %v\n", types.ConvertibleTo(t1, t2))
fmt.Println("SUCCESS: Type comparison functions work correctly\n")
}
func testTypeChecking() {
fmt.Println("\n=== Test Type Checking Functions ===")
pkg := types.NewPackage("example.com/test", "test")
m1 := types.NewFunc(token.NoPos, pkg, "Method1", types.NewSignatureType(nil, nil, nil, nil, nil, false))
m2 := types.NewFunc(token.NoPos, pkg, "Method2", types.NewSignatureType(nil, nil, nil, nil, nil, false))
iface := types.NewInterfaceType([]*types.Func{m1, m2}, nil)
iface.Complete()
fields := []*types.Var{
types.NewField(token.NoPos, nil, "x", types.Typ[types.Int], false),
}
structType := types.NewStruct(fields, nil)
fmt.Printf("Implements(struct, interface): %v\n", types.Implements(structType, iface))
fmt.Printf("Implements(int, interface): %v\n", types.Implements(types.Typ[types.Int], iface))
emptyIface := types.NewInterfaceType(nil, nil)
emptyIface.Complete()
fmt.Printf("Implements(int, empty interface): %v\n", types.Implements(types.Typ[types.Int], emptyIface))
fmt.Printf("AssertableTo(interface, int): %v\n", types.AssertableTo(iface, types.Typ[types.Int]))
fmt.Println("SUCCESS: Type checking functions work correctly\n")
}
func testStringFunctions() {
fmt.Println("\n=== Test String Functions ===")
pkg := types.NewPackage("example.com/test", "test")
varObj := types.NewVar(token.NoPos, pkg, "myVar", types.Typ[types.Int])
objStr := types.ObjectString(varObj, nil)
fmt.Printf("ObjectString: %s\n", objStr)
objStrQual := types.ObjectString(varObj, types.RelativeTo(pkg))
fmt.Printf("ObjectString (qualified): %s\n", objStrQual)
typeStr := types.TypeString(types.Typ[types.Int], nil)
fmt.Printf("TypeString(int): %s\n", typeStr)
sliceType := types.NewSlice(types.Typ[types.String])
sliceStr := types.TypeString(sliceType, nil)
fmt.Printf("TypeString([]string): %s\n", sliceStr)
fmt.Println("SUCCESS: String functions work correctly\n")
}
func testLookupFunctions() {
fmt.Println("\n=== Test Lookup Functions ===")
pkg := types.NewPackage("example.com/test", "test")
fields := []*types.Var{
types.NewField(token.NoPos, pkg, "X", types.Typ[types.Int], false),
types.NewField(token.NoPos, pkg, "Y", types.Typ[types.String], false),
}
structType := types.NewStruct(fields, nil)
obj, index, indirect := types.LookupFieldOrMethod(structType, false, pkg, "X")
if obj == nil {
panic("LookupFieldOrMethod failed to find X")
}
fmt.Printf("LookupFieldOrMethod found: %s, index: %v, indirect: %v\n", obj.Name(), index, indirect)
obj2, index2, indirect2 := types.LookupFieldOrMethod(structType, false, pkg, "NonExistent")
fmt.Printf("LookupFieldOrMethod (non-existent): found=%v, index=%v, indirect=%v\n", obj2 != nil, index2, indirect2)
mset := types.NewMethodSet(structType)
fmt.Printf("NewMethodSet: %d methods\n", mset.Len())
fmt.Println("SUCCESS: Lookup functions work correctly\n")
}
func testUtilityFunctions() {
fmt.Println("\n=== Test Utility Functions ===")
pkg := types.NewPackage("example.com/test", "test")
iface := types.NewInterfaceType(nil, nil)
iface.Complete()
fmt.Printf("IsInterface(interface): %v\n", types.IsInterface(iface))
fmt.Printf("IsInterface(int): %v\n", types.IsInterface(types.Typ[types.Int]))
typedNil := types.Typ[types.UntypedNil]
defaultType := types.Default(typedNil)
fmt.Printf("Default(UntypedNil): %v\n", defaultType)
intDefault := types.Default(types.Typ[types.Int])
fmt.Printf("Default(int): %v\n", intDefault)
idStr := types.Id(pkg, "MyType")
fmt.Printf("Id(pkg, \"MyType\"): %s\n", idStr)
fmt.Println("SUCCESS: Utility functions work correctly\n")
}

View File

@@ -0,0 +1,49 @@
package main
import (
"reflect"
)
func main() {
x := 42
p := &x
// Test 1: Non-pointer value - should return same value
v1 := reflect.Indirect(reflect.ValueOf(x))
if !v1.IsValid() || v1.Interface() != 42 {
panic("Non-pointer test failed: expected 42")
}
// Test 2: Pointer - should dereference
v2 := reflect.Indirect(reflect.ValueOf(p))
if !v2.IsValid() || v2.Interface() != 42 {
panic("Pointer dereference test failed: expected 42")
}
// Test 3: Nil pointer - should return invalid Value
var nilPtr *int
v3 := reflect.Indirect(reflect.ValueOf(nilPtr))
if v3.IsValid() {
panic("Nil pointer test failed: expected invalid Value")
}
// Test 4: Struct value - should return same value
type Person struct {
Name string
Age int
}
person := Person{Name: "Alice", Age: 30}
v4 := reflect.Indirect(reflect.ValueOf(person))
if !v4.IsValid() || v4.Interface().(Person).Name != "Alice" || v4.Interface().(Person).Age != 30 {
panic("Struct value test failed: expected Person{Name: Alice, Age: 30}")
}
// Test 5: Struct pointer - should dereference
personPtr := &Person{Name: "Bob", Age: 25}
v5 := reflect.Indirect(reflect.ValueOf(personPtr))
if !v5.IsValid() || v5.Interface().(Person).Name != "Bob" || v5.Interface().(Person).Age != 25 {
panic("Struct pointer test failed: expected Person{Name: Bob, Age: 25}")
}
println("PASS")
}

View File

@@ -0,0 +1,25 @@
package geometry1370
type Shape interface {
Area() float64
validate() bool
setID(int)
}
type Rectangle struct {
Width, Height float64
id int
}
func (r *Rectangle) Area() float64 { return r.Width * r.Height }
func (r *Rectangle) validate() bool { return r.Width > 0 && r.Height > 0 }
func (r *Rectangle) setID(id int) { r.id = id }
func (r *Rectangle) GetID() int { return r.id }
func NewRectangle(width, height float64) *Rectangle {
return &Rectangle{Width: width, Height: height}
}
func RegisterShape(s Shape, id int) {
s.setID(id)
}

View File

@@ -0,0 +1,90 @@
; ModuleID = 'github.com/goplus/llgo/cl/_testdata/geometry1370'
source_filename = "github.com/goplus/llgo/cl/_testdata/geometry1370"
%"github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle" = type { double, double, i64 }
%"github.com/goplus/llgo/runtime/internal/runtime.iface" = type { ptr, ptr }
@"github.com/goplus/llgo/cl/_testdata/geometry1370.init$guard" = global i1 false, align 1
define ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.NewRectangle"(double %0, double %1) {
_llgo_0:
%2 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocZ"(i64 24)
%3 = getelementptr inbounds %"github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", ptr %2, i32 0, i32 0
%4 = getelementptr inbounds %"github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", ptr %2, i32 0, i32 1
store double %0, ptr %3, align 8
store double %1, ptr %4, align 8
ret ptr %2
}
define double @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).Area"(ptr %0) {
_llgo_0:
%1 = getelementptr inbounds %"github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", ptr %0, i32 0, i32 0
%2 = load double, ptr %1, align 8
%3 = getelementptr inbounds %"github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", ptr %0, i32 0, i32 1
%4 = load double, ptr %3, align 8
%5 = fmul double %2, %4
ret double %5
}
define i64 @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).GetID"(ptr %0) {
_llgo_0:
%1 = getelementptr inbounds %"github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", ptr %0, i32 0, i32 2
%2 = load i64, ptr %1, align 4
ret i64 %2
}
define void @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).setID"(ptr %0, i64 %1) {
_llgo_0:
%2 = getelementptr inbounds %"github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", ptr %0, i32 0, i32 2
store i64 %1, ptr %2, align 4
ret void
}
define i1 @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).validate"(ptr %0) {
_llgo_0:
%1 = getelementptr inbounds %"github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", ptr %0, i32 0, i32 0
%2 = load double, ptr %1, align 8
%3 = fcmp ogt double %2, 0.000000e+00
br i1 %3, label %_llgo_1, label %_llgo_2
_llgo_1: ; preds = %_llgo_0
%4 = getelementptr inbounds %"github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", ptr %0, i32 0, i32 1
%5 = load double, ptr %4, align 8
%6 = fcmp ogt double %5, 0.000000e+00
br label %_llgo_2
_llgo_2: ; preds = %_llgo_1, %_llgo_0
%7 = phi i1 [ false, %_llgo_0 ], [ %6, %_llgo_1 ]
ret i1 %7
}
define void @"github.com/goplus/llgo/cl/_testdata/geometry1370.RegisterShape"(%"github.com/goplus/llgo/runtime/internal/runtime.iface" %0, i64 %1) {
_llgo_0:
%2 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.IfacePtrData"(%"github.com/goplus/llgo/runtime/internal/runtime.iface" %0)
%3 = extractvalue %"github.com/goplus/llgo/runtime/internal/runtime.iface" %0, 0
%4 = getelementptr ptr, ptr %3, i64 4
%5 = load ptr, ptr %4, align 8
%6 = insertvalue { ptr, ptr } undef, ptr %5, 0
%7 = insertvalue { ptr, ptr } %6, ptr %2, 1
%8 = extractvalue { ptr, ptr } %7, 1
%9 = extractvalue { ptr, ptr } %7, 0
call void %9(ptr %8, i64 %1)
ret void
}
define void @"github.com/goplus/llgo/cl/_testdata/geometry1370.init"() {
_llgo_0:
%0 = load i1, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.init$guard", align 1
br i1 %0, label %_llgo_2, label %_llgo_1
_llgo_1: ; preds = %_llgo_0
store i1 true, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.init$guard", align 1
br label %_llgo_2
_llgo_2: ; preds = %_llgo_1, %_llgo_0
ret void
}
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocZ"(i64)
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.IfacePtrData"(%"github.com/goplus/llgo/runtime/internal/runtime.iface")

View File

@@ -0,0 +1,51 @@
package main
func main() {
for _, label := range complexOrder() {
println(label)
}
}
func complexOrder() (res []string) {
record := func(label string) { res = append(res, label) }
defer record(label1("cleanup-final", 0))
defer record(label1("cleanup-before-loop", 0))
for i := 0; i < 2; i++ {
defer record(label1("exit-outer", i))
for j := 0; j < 2; j++ {
if j == 0 {
defer record(label2("branch-even", i, j))
} else {
defer record(label2("branch-odd", i, j))
}
for k := 0; k < 2; k++ {
nested := label3("nested", i, j, k)
defer record(nested)
if k == 1 {
defer record(label3("nested-tail", i, j, k))
}
}
}
}
defer record(label1("post-loop", 0))
return
}
func label1(prefix string, a int) string {
return prefix + "-" + digit(a)
}
func label2(prefix string, a, b int) string {
return prefix + "-" + digit(a) + "-" + digit(b)
}
func label3(prefix string, a, b, c int) string {
return prefix + "-" + digit(a) + "-" + digit(b) + "-" + digit(c)
}
func digit(n int) string {
return string(rune('0' + n))
}

View File

@@ -0,0 +1 @@
;

View File

@@ -0,0 +1,7 @@
package main
func main() {
for i := 0; i < 3; i++ {
defer println("loop", i)
}
}

View File

@@ -0,0 +1 @@
;

View File

@@ -0,0 +1,11 @@
package main
import (
"github.com/goplus/llgo/cl/_testdata/geometry1370"
)
func main() {
rect := geometry1370.NewRectangle(5.0, 3.0)
geometry1370.RegisterShape(rect, 42)
println("ID:", rect.GetID())
}

View File

@@ -0,0 +1,333 @@
; ModuleID = 'github.com/goplus/llgo/cl/_testgo/interface1370'
source_filename = "github.com/goplus/llgo/cl/_testgo/interface1370"
%"github.com/goplus/llgo/runtime/internal/runtime.iface" = type { ptr, ptr }
%"github.com/goplus/llgo/runtime/internal/runtime.String" = type { ptr, i64 }
%"github.com/goplus/llgo/runtime/abi.StructField" = type { %"github.com/goplus/llgo/runtime/internal/runtime.String", ptr, i64, %"github.com/goplus/llgo/runtime/internal/runtime.String", i1 }
%"github.com/goplus/llgo/runtime/internal/runtime.Slice" = type { ptr, i64, i64 }
%"github.com/goplus/llgo/runtime/abi.Method" = type { %"github.com/goplus/llgo/runtime/internal/runtime.String", ptr, ptr, ptr }
%"github.com/goplus/llgo/runtime/abi.Imethod" = type { %"github.com/goplus/llgo/runtime/internal/runtime.String", ptr }
@"github.com/goplus/llgo/cl/_testgo/interface1370.init$guard" = global i1 false, align 1
@"_llgo_github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle" = linkonce global ptr null, align 8
@0 = private unnamed_addr constant [48 x i8] c"github.com/goplus/llgo/cl/_testdata/geometry1370", align 1
@1 = private unnamed_addr constant [9 x i8] c"Rectangle", align 1
@_llgo_float64 = linkonce global ptr null, align 8
@_llgo_int = linkonce global ptr null, align 8
@"github.com/goplus/llgo/cl/_testgo/interface1370.struct$EuRbjzGGO7GwkW6RxZGl-8lEjTdEMzAFD8LnY_SpVoQ" = linkonce global ptr null, align 8
@2 = private unnamed_addr constant [5 x i8] c"Width", align 1
@3 = private unnamed_addr constant [6 x i8] c"Height", align 1
@4 = private unnamed_addr constant [2 x i8] c"id", align 1
@5 = private unnamed_addr constant [47 x i8] c"github.com/goplus/llgo/cl/_testgo/interface1370", align 1
@6 = private unnamed_addr constant [4 x i8] c"Area", align 1
@"_llgo_func$UYiLlmcWxoOKZPPzvR4LByitNeKoVGoTrB_5ubdOWW8" = linkonce global ptr null, align 8
@7 = private unnamed_addr constant [5 x i8] c"GetID", align 1
@"_llgo_func$ETeB8WwW04JEq0ztcm-XPTJtuYvtpkjIsAc0-2NT9zA" = linkonce global ptr null, align 8
@8 = private unnamed_addr constant [5 x i8] c"setID", align 1
@9 = private unnamed_addr constant [54 x i8] c"github.com/goplus/llgo/cl/_testdata/geometry1370.setID", align 1
@"_llgo_func$VZ-8VPNF1RaLICwxc1Ghn7BbgyFX3v762OCdx127EkA" = linkonce global ptr null, align 8
@10 = private unnamed_addr constant [8 x i8] c"validate", align 1
@11 = private unnamed_addr constant [57 x i8] c"github.com/goplus/llgo/cl/_testdata/geometry1370.validate", align 1
@_llgo_bool = linkonce global ptr null, align 8
@"_llgo_func$YHeRw3AOvQtzv982-ZO3Yn8vh3Fx89RM3VvI8E4iKVk" = linkonce global ptr null, align 8
@"*_llgo_github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle" = linkonce global ptr null, align 8
@"github.com/goplus/llgo/cl/_testgo/interface1370.iface$OopIVfjRcxQr1gmJyGi5G7hHt__vH05AREEM7PthH9o" = linkonce global ptr null, align 8
@12 = private unnamed_addr constant [3 x i8] c"ID:", align 1
define void @"github.com/goplus/llgo/cl/_testgo/interface1370.init"() {
_llgo_0:
%0 = load i1, ptr @"github.com/goplus/llgo/cl/_testgo/interface1370.init$guard", align 1
br i1 %0, label %_llgo_2, label %_llgo_1
_llgo_1: ; preds = %_llgo_0
store i1 true, ptr @"github.com/goplus/llgo/cl/_testgo/interface1370.init$guard", align 1
call void @"github.com/goplus/llgo/cl/_testdata/geometry1370.init"()
call void @"github.com/goplus/llgo/cl/_testgo/interface1370.init$after"()
br label %_llgo_2
_llgo_2: ; preds = %_llgo_1, %_llgo_0
ret void
}
define void @"github.com/goplus/llgo/cl/_testgo/interface1370.main"() {
_llgo_0:
%0 = call ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.NewRectangle"(double 5.000000e+00, double 3.000000e+00)
%1 = load ptr, ptr @"_llgo_github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", align 8
%2 = load ptr, ptr @"*_llgo_github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", align 8
%3 = load ptr, ptr @"_llgo_func$UYiLlmcWxoOKZPPzvR4LByitNeKoVGoTrB_5ubdOWW8", align 8
%4 = load ptr, ptr @"_llgo_func$VZ-8VPNF1RaLICwxc1Ghn7BbgyFX3v762OCdx127EkA", align 8
%5 = load ptr, ptr @"_llgo_func$YHeRw3AOvQtzv982-ZO3Yn8vh3Fx89RM3VvI8E4iKVk", align 8
%6 = load ptr, ptr @"github.com/goplus/llgo/cl/_testgo/interface1370.iface$OopIVfjRcxQr1gmJyGi5G7hHt__vH05AREEM7PthH9o", align 8
%7 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.NewItab"(ptr %6, ptr %2)
%8 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.iface" undef, ptr %7, 0
%9 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.iface" %8, ptr %0, 1
call void @"github.com/goplus/llgo/cl/_testdata/geometry1370.RegisterShape"(%"github.com/goplus/llgo/runtime/internal/runtime.iface" %9, i64 42)
%10 = call i64 @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).GetID"(ptr %0)
call void @"github.com/goplus/llgo/runtime/internal/runtime.PrintString"(%"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @12, i64 3 })
call void @"github.com/goplus/llgo/runtime/internal/runtime.PrintByte"(i8 32)
call void @"github.com/goplus/llgo/runtime/internal/runtime.PrintInt"(i64 %10)
call void @"github.com/goplus/llgo/runtime/internal/runtime.PrintByte"(i8 10)
ret void
}
declare void @"github.com/goplus/llgo/cl/_testdata/geometry1370.init"()
declare ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.NewRectangle"(double, double)
declare void @"github.com/goplus/llgo/cl/_testdata/geometry1370.RegisterShape"(%"github.com/goplus/llgo/runtime/internal/runtime.iface", i64)
define void @"github.com/goplus/llgo/cl/_testgo/interface1370.init$after"() {
_llgo_0:
%0 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.NewNamed"(%"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @0, i64 48 }, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @1, i64 9 }, i64 25, i64 24, i64 0, i64 4)
%1 = load ptr, ptr @"_llgo_github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", align 8
%2 = icmp eq ptr %1, null
br i1 %2, label %_llgo_1, label %_llgo_2
_llgo_1: ; preds = %_llgo_0
store ptr %0, ptr @"_llgo_github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", align 8
br label %_llgo_2
_llgo_2: ; preds = %_llgo_1, %_llgo_0
%3 = load ptr, ptr @_llgo_float64, align 8
%4 = icmp eq ptr %3, null
br i1 %4, label %_llgo_3, label %_llgo_4
_llgo_3: ; preds = %_llgo_2
%5 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 46)
store ptr %5, ptr @_llgo_float64, align 8
br label %_llgo_4
_llgo_4: ; preds = %_llgo_3, %_llgo_2
%6 = load ptr, ptr @_llgo_float64, align 8
%7 = load ptr, ptr @_llgo_int, align 8
%8 = icmp eq ptr %7, null
br i1 %8, label %_llgo_5, label %_llgo_6
_llgo_5: ; preds = %_llgo_4
%9 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 34)
store ptr %9, ptr @_llgo_int, align 8
br label %_llgo_6
_llgo_6: ; preds = %_llgo_5, %_llgo_4
%10 = load ptr, ptr @_llgo_int, align 8
%11 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 46)
%12 = call %"github.com/goplus/llgo/runtime/abi.StructField" @"github.com/goplus/llgo/runtime/internal/runtime.StructField"(%"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @2, i64 5 }, ptr %11, i64 0, %"github.com/goplus/llgo/runtime/internal/runtime.String" zeroinitializer, i1 false)
%13 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 46)
%14 = call %"github.com/goplus/llgo/runtime/abi.StructField" @"github.com/goplus/llgo/runtime/internal/runtime.StructField"(%"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @3, i64 6 }, ptr %13, i64 8, %"github.com/goplus/llgo/runtime/internal/runtime.String" zeroinitializer, i1 false)
%15 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 34)
%16 = call %"github.com/goplus/llgo/runtime/abi.StructField" @"github.com/goplus/llgo/runtime/internal/runtime.StructField"(%"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @4, i64 2 }, ptr %15, i64 16, %"github.com/goplus/llgo/runtime/internal/runtime.String" zeroinitializer, i1 false)
%17 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 168)
%18 = getelementptr %"github.com/goplus/llgo/runtime/abi.StructField", ptr %17, i64 0
store %"github.com/goplus/llgo/runtime/abi.StructField" %12, ptr %18, align 8
%19 = getelementptr %"github.com/goplus/llgo/runtime/abi.StructField", ptr %17, i64 1
store %"github.com/goplus/llgo/runtime/abi.StructField" %14, ptr %19, align 8
%20 = getelementptr %"github.com/goplus/llgo/runtime/abi.StructField", ptr %17, i64 2
store %"github.com/goplus/llgo/runtime/abi.StructField" %16, ptr %20, align 8
%21 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %17, 0
%22 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %21, i64 3, 1
%23 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %22, i64 3, 2
%24 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Struct"(%"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @5, i64 47 }, i64 24, %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %23)
store ptr %24, ptr @"github.com/goplus/llgo/cl/_testgo/interface1370.struct$EuRbjzGGO7GwkW6RxZGl-8lEjTdEMzAFD8LnY_SpVoQ", align 8
%25 = load ptr, ptr @"github.com/goplus/llgo/cl/_testgo/interface1370.struct$EuRbjzGGO7GwkW6RxZGl-8lEjTdEMzAFD8LnY_SpVoQ", align 8
br i1 %2, label %_llgo_7, label %_llgo_8
_llgo_7: ; preds = %_llgo_6
%26 = load ptr, ptr @"_llgo_func$UYiLlmcWxoOKZPPzvR4LByitNeKoVGoTrB_5ubdOWW8", align 8
%27 = icmp eq ptr %26, null
br i1 %27, label %_llgo_9, label %_llgo_10
_llgo_8: ; preds = %_llgo_18, %_llgo_6
%28 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.NewNamed"(%"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @0, i64 48 }, %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @1, i64 9 }, i64 25, i64 24, i64 0, i64 4)
%29 = load ptr, ptr @"*_llgo_github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", align 8
%30 = icmp eq ptr %29, null
br i1 %30, label %_llgo_19, label %_llgo_20
_llgo_9: ; preds = %_llgo_7
%31 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 0)
%32 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %31, 0
%33 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %32, i64 0, 1
%34 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %33, i64 0, 2
%35 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 46)
%36 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 8)
%37 = getelementptr ptr, ptr %36, i64 0
store ptr %35, ptr %37, align 8
%38 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %36, 0
%39 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %38, i64 1, 1
%40 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %39, i64 1, 2
%41 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Func"(%"github.com/goplus/llgo/runtime/internal/runtime.Slice" %34, %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %40, i1 false)
call void @"github.com/goplus/llgo/runtime/internal/runtime.SetDirectIface"(ptr %41)
store ptr %41, ptr @"_llgo_func$UYiLlmcWxoOKZPPzvR4LByitNeKoVGoTrB_5ubdOWW8", align 8
br label %_llgo_10
_llgo_10: ; preds = %_llgo_9, %_llgo_7
%42 = load ptr, ptr @"_llgo_func$UYiLlmcWxoOKZPPzvR4LByitNeKoVGoTrB_5ubdOWW8", align 8
%43 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" { %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @6, i64 4 }, ptr undef, ptr undef, ptr undef }, ptr %42, 1
%44 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" %43, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).Area", 2
%45 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" %44, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).Area", 3
%46 = load ptr, ptr @"_llgo_func$ETeB8WwW04JEq0ztcm-XPTJtuYvtpkjIsAc0-2NT9zA", align 8
%47 = icmp eq ptr %46, null
br i1 %47, label %_llgo_11, label %_llgo_12
_llgo_11: ; preds = %_llgo_10
%48 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 0)
%49 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %48, 0
%50 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %49, i64 0, 1
%51 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %50, i64 0, 2
%52 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 34)
%53 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 8)
%54 = getelementptr ptr, ptr %53, i64 0
store ptr %52, ptr %54, align 8
%55 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %53, 0
%56 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %55, i64 1, 1
%57 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %56, i64 1, 2
%58 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Func"(%"github.com/goplus/llgo/runtime/internal/runtime.Slice" %51, %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %57, i1 false)
call void @"github.com/goplus/llgo/runtime/internal/runtime.SetDirectIface"(ptr %58)
store ptr %58, ptr @"_llgo_func$ETeB8WwW04JEq0ztcm-XPTJtuYvtpkjIsAc0-2NT9zA", align 8
br label %_llgo_12
_llgo_12: ; preds = %_llgo_11, %_llgo_10
%59 = load ptr, ptr @"_llgo_func$ETeB8WwW04JEq0ztcm-XPTJtuYvtpkjIsAc0-2NT9zA", align 8
%60 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" { %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @7, i64 5 }, ptr undef, ptr undef, ptr undef }, ptr %59, 1
%61 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" %60, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).GetID", 2
%62 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" %61, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).GetID", 3
%63 = load ptr, ptr @"_llgo_func$VZ-8VPNF1RaLICwxc1Ghn7BbgyFX3v762OCdx127EkA", align 8
%64 = icmp eq ptr %63, null
br i1 %64, label %_llgo_13, label %_llgo_14
_llgo_13: ; preds = %_llgo_12
%65 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 34)
%66 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 8)
%67 = getelementptr ptr, ptr %66, i64 0
store ptr %65, ptr %67, align 8
%68 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %66, 0
%69 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %68, i64 1, 1
%70 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %69, i64 1, 2
%71 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 0)
%72 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %71, 0
%73 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %72, i64 0, 1
%74 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %73, i64 0, 2
%75 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Func"(%"github.com/goplus/llgo/runtime/internal/runtime.Slice" %70, %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %74, i1 false)
call void @"github.com/goplus/llgo/runtime/internal/runtime.SetDirectIface"(ptr %75)
store ptr %75, ptr @"_llgo_func$VZ-8VPNF1RaLICwxc1Ghn7BbgyFX3v762OCdx127EkA", align 8
br label %_llgo_14
_llgo_14: ; preds = %_llgo_13, %_llgo_12
%76 = load ptr, ptr @"_llgo_func$VZ-8VPNF1RaLICwxc1Ghn7BbgyFX3v762OCdx127EkA", align 8
%77 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" { %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @9, i64 54 }, ptr undef, ptr undef, ptr undef }, ptr %76, 1
%78 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" %77, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).setID", 2
%79 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" %78, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).setID", 3
%80 = load ptr, ptr @_llgo_bool, align 8
%81 = icmp eq ptr %80, null
br i1 %81, label %_llgo_15, label %_llgo_16
_llgo_15: ; preds = %_llgo_14
%82 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 33)
store ptr %82, ptr @_llgo_bool, align 8
br label %_llgo_16
_llgo_16: ; preds = %_llgo_15, %_llgo_14
%83 = load ptr, ptr @_llgo_bool, align 8
%84 = load ptr, ptr @"_llgo_func$YHeRw3AOvQtzv982-ZO3Yn8vh3Fx89RM3VvI8E4iKVk", align 8
%85 = icmp eq ptr %84, null
br i1 %85, label %_llgo_17, label %_llgo_18
_llgo_17: ; preds = %_llgo_16
%86 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 0)
%87 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %86, 0
%88 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %87, i64 0, 1
%89 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %88, i64 0, 2
%90 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64 33)
%91 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 8)
%92 = getelementptr ptr, ptr %91, i64 0
store ptr %90, ptr %92, align 8
%93 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %91, 0
%94 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %93, i64 1, 1
%95 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %94, i64 1, 2
%96 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Func"(%"github.com/goplus/llgo/runtime/internal/runtime.Slice" %89, %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %95, i1 false)
call void @"github.com/goplus/llgo/runtime/internal/runtime.SetDirectIface"(ptr %96)
store ptr %96, ptr @"_llgo_func$YHeRw3AOvQtzv982-ZO3Yn8vh3Fx89RM3VvI8E4iKVk", align 8
br label %_llgo_18
_llgo_18: ; preds = %_llgo_17, %_llgo_16
%97 = load ptr, ptr @"_llgo_func$YHeRw3AOvQtzv982-ZO3Yn8vh3Fx89RM3VvI8E4iKVk", align 8
%98 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" { %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @11, i64 57 }, ptr undef, ptr undef, ptr undef }, ptr %97, 1
%99 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" %98, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).validate", 2
%100 = insertvalue %"github.com/goplus/llgo/runtime/abi.Method" %99, ptr @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).validate", 3
%101 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 160)
%102 = getelementptr %"github.com/goplus/llgo/runtime/abi.Method", ptr %101, i64 0
store %"github.com/goplus/llgo/runtime/abi.Method" %45, ptr %102, align 8
%103 = getelementptr %"github.com/goplus/llgo/runtime/abi.Method", ptr %101, i64 1
store %"github.com/goplus/llgo/runtime/abi.Method" %62, ptr %103, align 8
%104 = getelementptr %"github.com/goplus/llgo/runtime/abi.Method", ptr %101, i64 2
store %"github.com/goplus/llgo/runtime/abi.Method" %79, ptr %104, align 8
%105 = getelementptr %"github.com/goplus/llgo/runtime/abi.Method", ptr %101, i64 3
store %"github.com/goplus/llgo/runtime/abi.Method" %100, ptr %105, align 8
%106 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %101, 0
%107 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %106, i64 4, 1
%108 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %107, i64 4, 2
call void @"github.com/goplus/llgo/runtime/internal/runtime.InitNamed"(ptr %0, ptr %25, { ptr, i64, i64 } zeroinitializer, %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %108)
br label %_llgo_8
_llgo_19: ; preds = %_llgo_8
%109 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.PointerTo"(ptr %28)
call void @"github.com/goplus/llgo/runtime/internal/runtime.SetDirectIface"(ptr %109)
store ptr %109, ptr @"*_llgo_github.com/goplus/llgo/cl/_testdata/geometry1370.Rectangle", align 8
br label %_llgo_20
_llgo_20: ; preds = %_llgo_19, %_llgo_8
%110 = load ptr, ptr @"_llgo_func$UYiLlmcWxoOKZPPzvR4LByitNeKoVGoTrB_5ubdOWW8", align 8
%111 = load ptr, ptr @"_llgo_func$VZ-8VPNF1RaLICwxc1Ghn7BbgyFX3v762OCdx127EkA", align 8
%112 = load ptr, ptr @"_llgo_func$YHeRw3AOvQtzv982-ZO3Yn8vh3Fx89RM3VvI8E4iKVk", align 8
%113 = insertvalue %"github.com/goplus/llgo/runtime/abi.Imethod" { %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @6, i64 4 }, ptr undef }, ptr %110, 1
%114 = insertvalue %"github.com/goplus/llgo/runtime/abi.Imethod" { %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @9, i64 54 }, ptr undef }, ptr %111, 1
%115 = insertvalue %"github.com/goplus/llgo/runtime/abi.Imethod" { %"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @11, i64 57 }, ptr undef }, ptr %112, 1
%116 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64 72)
%117 = getelementptr %"github.com/goplus/llgo/runtime/abi.Imethod", ptr %116, i64 0
store %"github.com/goplus/llgo/runtime/abi.Imethod" %113, ptr %117, align 8
%118 = getelementptr %"github.com/goplus/llgo/runtime/abi.Imethod", ptr %116, i64 1
store %"github.com/goplus/llgo/runtime/abi.Imethod" %114, ptr %118, align 8
%119 = getelementptr %"github.com/goplus/llgo/runtime/abi.Imethod", ptr %116, i64 2
store %"github.com/goplus/llgo/runtime/abi.Imethod" %115, ptr %119, align 8
%120 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" undef, ptr %116, 0
%121 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %120, i64 3, 1
%122 = insertvalue %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %121, i64 3, 2
%123 = call ptr @"github.com/goplus/llgo/runtime/internal/runtime.Interface"(%"github.com/goplus/llgo/runtime/internal/runtime.String" { ptr @5, i64 47 }, %"github.com/goplus/llgo/runtime/internal/runtime.Slice" %122)
store ptr %123, ptr @"github.com/goplus/llgo/cl/_testgo/interface1370.iface$OopIVfjRcxQr1gmJyGi5G7hHt__vH05AREEM7PthH9o", align 8
ret void
}
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.NewNamed"(%"github.com/goplus/llgo/runtime/internal/runtime.String", %"github.com/goplus/llgo/runtime/internal/runtime.String", i64, i64, i64, i64)
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.Basic"(i64)
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.Struct"(%"github.com/goplus/llgo/runtime/internal/runtime.String", i64, %"github.com/goplus/llgo/runtime/internal/runtime.Slice")
declare %"github.com/goplus/llgo/runtime/abi.StructField" @"github.com/goplus/llgo/runtime/internal/runtime.StructField"(%"github.com/goplus/llgo/runtime/internal/runtime.String", ptr, i64, %"github.com/goplus/llgo/runtime/internal/runtime.String", i1)
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.AllocU"(i64)
declare void @"github.com/goplus/llgo/runtime/internal/runtime.InitNamed"(ptr, ptr, %"github.com/goplus/llgo/runtime/internal/runtime.Slice", %"github.com/goplus/llgo/runtime/internal/runtime.Slice")
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.Func"(%"github.com/goplus/llgo/runtime/internal/runtime.Slice", %"github.com/goplus/llgo/runtime/internal/runtime.Slice", i1)
declare void @"github.com/goplus/llgo/runtime/internal/runtime.SetDirectIface"(ptr)
declare double @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).Area"(ptr)
declare i64 @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).GetID"(ptr)
declare void @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).setID"(ptr, i64)
declare i1 @"github.com/goplus/llgo/cl/_testdata/geometry1370.(*Rectangle).validate"(ptr)
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.PointerTo"(ptr)
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.Interface"(%"github.com/goplus/llgo/runtime/internal/runtime.String", %"github.com/goplus/llgo/runtime/internal/runtime.Slice")
declare ptr @"github.com/goplus/llgo/runtime/internal/runtime.NewItab"(ptr, ptr)
declare void @"github.com/goplus/llgo/runtime/internal/runtime.PrintString"(%"github.com/goplus/llgo/runtime/internal/runtime.String")
declare void @"github.com/goplus/llgo/runtime/internal/runtime.PrintByte"(i8)
declare void @"github.com/goplus/llgo/runtime/internal/runtime.PrintInt"(i64)

View File

@@ -0,0 +1,11 @@
package dep
import "fmt"
var VarName = "dep-default"
var VarPlain string
func PrintVar() {
fmt.Printf("dep.VarName: %s\n", VarName)
fmt.Printf("dep.VarPlain: %s\n", VarPlain)
}

View File

@@ -0,0 +1,23 @@
package main
import (
"fmt"
"runtime"
dep "github.com/goplus/llgo/cl/_testgo/rewrite/dep"
)
var VarName = "main-default"
var VarPlain string
func printLine(label, value string) {
fmt.Printf("%s: %s\n", label, value)
}
func main() {
printLine("main.VarName", VarName)
printLine("main.VarPlain", VarPlain)
dep.PrintVar()
printLine("runtime.GOROOT()", runtime.GOROOT())
printLine("runtime.Version()", runtime.Version())
}

View File

@@ -0,0 +1 @@
;

View File

@@ -0,0 +1,14 @@
package main
import (
"unsafe"
_ "unsafe"
)
//go:linkname getsp llgo.stackSave
func getsp() unsafe.Pointer
func main() {
sp := getsp()
println(sp)
}

View File

@@ -0,0 +1,34 @@
; ModuleID = 'github.com/goplus/llgo/cl/_testrt/stacksave'
source_filename = "github.com/goplus/llgo/cl/_testrt/stacksave"
@"github.com/goplus/llgo/cl/_testrt/stacksave.init$guard" = global i1 false, align 1
define void @"github.com/goplus/llgo/cl/_testrt/stacksave.init"() {
_llgo_0:
%0 = load i1, ptr @"github.com/goplus/llgo/cl/_testrt/stacksave.init$guard", align 1
br i1 %0, label %_llgo_2, label %_llgo_1
_llgo_1: ; preds = %_llgo_0
store i1 true, ptr @"github.com/goplus/llgo/cl/_testrt/stacksave.init$guard", align 1
br label %_llgo_2
_llgo_2: ; preds = %_llgo_1, %_llgo_0
ret void
}
define void @"github.com/goplus/llgo/cl/_testrt/stacksave.main"() {
_llgo_0:
%0 = call ptr @llvm.stacksave()
call void @"github.com/goplus/llgo/runtime/internal/runtime.PrintPointer"(ptr %0)
call void @"github.com/goplus/llgo/runtime/internal/runtime.PrintByte"(i8 10)
ret void
}
; Function Attrs: nocallback nofree nosync nounwind willreturn
declare ptr @llvm.stacksave() #0
declare void @"github.com/goplus/llgo/runtime/internal/runtime.PrintPointer"(ptr)
declare void @"github.com/goplus/llgo/runtime/internal/runtime.PrintByte"(i8)
attributes #0 = { nocallback nofree nosync nounwind willreturn }

View File

@@ -393,10 +393,10 @@ func TestErrImport(t *testing.T) {
func TestErrInitLinkname(t *testing.T) {
var ctx context
ctx.initLinkname("//llgo:link abc", func(name string) (string, bool, bool) {
ctx.initLinkname("//llgo:link abc", func(name string, isExport bool) (string, bool, bool) {
return "", false, false
})
ctx.initLinkname("//go:linkname Printf printf", func(name string) (string, bool, bool) {
ctx.initLinkname("//go:linkname Printf printf", func(name string, isExport bool) (string, bool, bool) {
return "", false, false
})
defer func() {
@@ -404,7 +404,7 @@ func TestErrInitLinkname(t *testing.T) {
t.Fatal("initLinkname: no error?")
}
}()
ctx.initLinkname("//go:linkname Printf printf", func(name string) (string, bool, bool) {
ctx.initLinkname("//go:linkname Printf printf", func(name string, isExport bool) (string, bool, bool) {
return "foo.Printf", false, name == "Printf"
})
}
@@ -506,3 +506,238 @@ func TestInstantiate(t *testing.T) {
t.Fatal("error")
}
}
func TestHandleExportDiffName(t *testing.T) {
tests := []struct {
name string
enableExportRename bool
line string
fullName string
inPkgName string
wantHasLinkname bool
wantLinkname string
wantExport string
}{
{
name: "ExportDiffNames_DifferentName",
enableExportRename: true,
line: "//export IRQ_Handler",
fullName: "pkg.HandleInterrupt",
inPkgName: "HandleInterrupt",
wantHasLinkname: true,
wantLinkname: "IRQ_Handler",
wantExport: "IRQ_Handler",
},
{
name: "ExportDiffNames_SameName",
enableExportRename: true,
line: "//export SameName",
fullName: "pkg.SameName",
inPkgName: "SameName",
wantHasLinkname: true,
wantLinkname: "SameName",
wantExport: "SameName",
},
{
name: "ExportDiffNames_WithSpaces",
enableExportRename: true,
line: "//export Timer_Callback ",
fullName: "pkg.OnTimerTick",
inPkgName: "OnTimerTick",
wantHasLinkname: true,
wantLinkname: "Timer_Callback",
wantExport: "Timer_Callback",
},
{
name: "ExportDiffNames_Disabled_MatchingName",
enableExportRename: false,
line: "//export Func",
fullName: "pkg.Func",
inPkgName: "Func",
wantHasLinkname: true,
wantLinkname: "Func",
wantExport: "Func",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Save and restore global state
oldEnableExportRename := enableExportRename
defer func() {
EnableExportRename(oldEnableExportRename)
}()
EnableExportRename(tt.enableExportRename)
// Setup context
prog := llssa.NewProgram(nil)
pkg := prog.NewPackage("test", "test")
ctx := &context{
prog: prog,
pkg: pkg,
}
// Call initLinkname with closure that mimics initLinknameByDoc behavior
ret := ctx.initLinkname(tt.line, func(name string, isExport bool) (string, bool, bool) {
return tt.fullName, false, name == tt.inPkgName || (isExport && enableExportRename)
})
// Verify result
hasLinkname := (ret == hasLinkname)
if hasLinkname != tt.wantHasLinkname {
t.Errorf("hasLinkname = %v, want %v", hasLinkname, tt.wantHasLinkname)
}
if tt.wantHasLinkname {
// Check linkname was set
if link, ok := prog.Linkname(tt.fullName); !ok || link != tt.wantLinkname {
t.Errorf("linkname = %q (ok=%v), want %q", link, ok, tt.wantLinkname)
}
// Check export was set
exports := pkg.ExportFuncs()
if export, ok := exports[tt.fullName]; !ok || export != tt.wantExport {
t.Errorf("export = %q (ok=%v), want %q", export, ok, tt.wantExport)
}
}
})
}
}
func TestInitLinknameByDocExportDiffNames(t *testing.T) {
tests := []struct {
name string
enableExportRename bool
doc *ast.CommentGroup
fullName string
inPkgName string
wantExported bool // Whether the symbol should be exported with different name
wantLinkname string
wantExport string
}{
{
name: "WithExportDiffNames_DifferentNameExported",
enableExportRename: true,
doc: &ast.CommentGroup{
List: []*ast.Comment{
{Text: "//export IRQ_Handler"},
},
},
fullName: "pkg.HandleInterrupt",
inPkgName: "HandleInterrupt",
wantExported: true,
wantLinkname: "IRQ_Handler",
wantExport: "IRQ_Handler",
},
{
name: "WithoutExportDiffNames_NotExported",
enableExportRename: false,
doc: &ast.CommentGroup{
List: []*ast.Comment{
{Text: "//export DifferentName"},
},
},
fullName: "pkg.HandleInterrupt",
inPkgName: "HandleInterrupt",
wantExported: false,
// Without enableExportRename, it goes through normal flow which expects same name
// The symbol "DifferentName" won't be found, so no export happens
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Without enableExportRename, export with different name will panic
if !tt.wantExported && !tt.enableExportRename {
defer func() {
if r := recover(); r == nil {
t.Error("expected panic for export with different name when enableExportRename=false")
}
}()
}
// Save and restore global state
oldEnableExportRename := enableExportRename
defer func() {
EnableExportRename(oldEnableExportRename)
}()
EnableExportRename(tt.enableExportRename)
// Setup context
prog := llssa.NewProgram(nil)
pkg := prog.NewPackage("test", "test")
ctx := &context{
prog: prog,
pkg: pkg,
}
// Call initLinknameByDoc
ctx.initLinknameByDoc(tt.doc, tt.fullName, tt.inPkgName, false)
// Verify export behavior
exports := pkg.ExportFuncs()
if tt.wantExported {
// Should have exported the symbol with different name
if export, ok := exports[tt.fullName]; !ok || export != tt.wantExport {
t.Errorf("export = %q (ok=%v), want %q", export, ok, tt.wantExport)
}
// Check linkname was also set
if link, ok := prog.Linkname(tt.fullName); !ok || link != tt.wantLinkname {
t.Errorf("linkname = %q (ok=%v), want %q", link, ok, tt.wantLinkname)
}
}
})
}
}
func TestInitLinkExportDiffNames(t *testing.T) {
tests := []struct {
name string
enableExportRename bool
line string
wantPanic bool
}{
{
name: "ExportDiffNames_Enabled_NoError",
enableExportRename: true,
line: "//export IRQ_Handler",
wantPanic: false,
},
{
name: "ExportDiffNames_Disabled_Panic",
enableExportRename: false,
line: "//export IRQ_Handler",
wantPanic: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.wantPanic {
defer func() {
if r := recover(); r == nil {
t.Error("expected panic but didn't panic")
}
}()
}
oldEnableExportRename := enableExportRename
defer func() {
EnableExportRename(oldEnableExportRename)
}()
EnableExportRename(tt.enableExportRename)
prog := llssa.NewProgram(nil)
pkg := prog.NewPackage("test", "test")
ctx := &context{
prog: prog,
pkg: pkg,
}
ctx.initLinkname(tt.line, func(inPkgName string, isExport bool) (fullName string, isVar, ok bool) {
// Simulate initLinknames scenario: symbol not found (like in decl packages)
return "", false, false
})
})
}
}

View File

@@ -53,6 +53,11 @@ var (
enableDbg bool
enableDbgSyms bool
disableInline bool
// enableExportRename enables //export to use different C symbol names than Go function names.
// This is for TinyGo compatibility when using -target flag for embedded targets.
// Currently, using -target implies TinyGo embedded target mode.
enableExportRename bool
)
// SetDebug sets debug flags.
@@ -73,6 +78,12 @@ func EnableTrace(b bool) {
enableCallTracing = b
}
// EnableExportRename enables or disables //export with different C symbol names.
// This is enabled when using -target flag for TinyGo compatibility.
func EnableExportRename(b bool) {
enableExportRename = b
}
// -----------------------------------------------------------------------------
type instrOrValue interface {
@@ -110,6 +121,7 @@ type context struct {
loaded map[*types.Package]*pkgInfo // loaded packages
bvals map[ssa.Value]llssa.Expr // block values
vargs map[*ssa.Alloc][]llssa.Expr // varargs
paramDIVars map[*types.Var]llssa.DIVar
patches Patches
blkInfos []blocks.Info
@@ -126,6 +138,58 @@ type context struct {
cgoArgs []llssa.Expr
cgoRet llssa.Expr
cgoSymbols []string
rewrites map[string]string
}
func (p *context) rewriteValue(name string) (string, bool) {
if p.rewrites == nil {
return "", false
}
dot := strings.LastIndex(name, ".")
if dot < 0 || dot == len(name)-1 {
return "", false
}
varName := name[dot+1:]
val, ok := p.rewrites[varName]
return val, ok
}
// isStringPtrType checks if typ is a pointer to the basic string type (*string).
// This is used to validate that -ldflags -X can only rewrite variables of type *string,
// not derived string types like "type T string".
func (p *context) isStringPtrType(typ types.Type) bool {
ptr, ok := typ.(*types.Pointer)
if !ok {
return false
}
basic, ok := ptr.Elem().(*types.Basic)
return ok && basic.Kind() == types.String
}
func (p *context) globalFullName(g *ssa.Global) string {
name, _, _ := p.varName(g.Pkg.Pkg, g)
return name
}
func (p *context) rewriteInitStore(store *ssa.Store, g *ssa.Global) (string, bool) {
if p.rewrites == nil {
return "", false
}
fn := store.Block().Parent()
if fn == nil || fn.Synthetic != "package initializer" {
return "", false
}
if _, ok := store.Val.(*ssa.Const); !ok {
return "", false
}
if !p.isStringPtrType(g.Type()) {
return "", false
}
value, ok := p.rewriteValue(p.globalFullName(g))
if !ok {
return "", false
}
return value, true
}
type pkgState byte
@@ -175,9 +239,18 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) {
log.Println("==> NewVar", name, typ)
}
g := pkg.NewVar(name, typ, llssa.Background(vtype))
if value, ok := p.rewriteValue(name); ok {
if p.isStringPtrType(gbl.Type()) {
g.Init(pkg.ConstString(value))
} else {
log.Printf("warning: ignoring rewrite for non-string variable %s (type: %v)", name, gbl.Type())
if define {
g.InitNil()
}
}
} else if define {
g.InitNil()
}
}
func makeClosureCtx(pkg *types.Package, vars []*ssa.FreeVar) *types.Var {
@@ -263,6 +336,8 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
if f.Recover != nil { // set recover block
fn.SetRecover(fn.Block(f.Recover.Index))
}
dbgEnabled := enableDbg && (f == nil || f.Origin() == nil)
dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil)
p.inits = append(p.inits, func() {
p.fn = fn
p.state = state // restore pkgState when compiling funcBody
@@ -270,6 +345,11 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
p.fn = nil
}()
p.phis = nil
if dbgSymsEnabled {
p.paramDIVars = make(map[*types.Var]llssa.DIVar)
} else {
p.paramDIVars = nil
}
if debugGoSSA {
f.WriteTo(os.Stderr)
}
@@ -277,7 +357,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
log.Println("==> FuncBody", name)
}
b := fn.NewBuilder()
if enableDbg {
if dbgEnabled {
pos := p.goProg.Fset.Position(f.Pos())
bodyPos := p.getFuncBodyPos(f)
b.DebugFunction(fn, pos, bodyPos)
@@ -371,6 +451,9 @@ func (p *context) debugParams(b llssa.Builder, f *ssa.Function) {
ty := param.Type()
argNo := i + 1
div := b.DIVarParam(p.fn, pos, param.Name(), p.type_(ty, llssa.InGo), argNo)
if p.paramDIVars != nil {
p.paramDIVars[variable] = div
}
b.DIParam(variable, v, div, p.fn, pos, p.fn.Block(0))
}
}
@@ -388,7 +471,7 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do
b.Printf("call " + fn.Name() + "\n\x00")
}
// place here to avoid wrong current-block
if enableDbgSyms && block.Index == 0 {
if enableDbgSyms && block.Parent().Origin() == nil && block.Index == 0 {
p.debugParams(b, block.Parent())
}
if doModInit {
@@ -783,7 +866,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
p.compileInstrOrValue(b, iv, false)
return
}
if enableDbg {
if enableDbg && instr.Parent().Origin() == nil {
scope := p.getDebugLocScope(instr.Parent(), instr.Pos())
if scope != nil {
diScope := b.DIScope(p.fn, scope)
@@ -805,6 +888,13 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
return
}
}
if p.rewrites != nil {
if g, ok := va.(*ssa.Global); ok {
if _, ok := p.rewriteInitStore(v, g); ok {
return
}
}
}
ptr := p.compileValue(b, va)
val := p.compileValue(b, v.Val)
b.Store(ptr, val)
@@ -846,7 +936,7 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
x := p.compileValue(b, v.X)
b.Send(ch, x)
case *ssa.DebugRef:
if enableDbgSyms {
if enableDbgSyms && v.Parent().Origin() == nil {
p.debugRef(b, v)
}
default:
@@ -855,14 +945,13 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) {
}
func (p *context) getLocalVariable(b llssa.Builder, fn *ssa.Function, v *types.Var) llssa.DIVar {
if p.paramDIVars != nil {
if div, ok := p.paramDIVars[v]; ok {
return div
}
}
pos := p.fset.Position(v.Pos())
t := p.type_(v.Type(), llssa.InGo)
for i, param := range fn.Params {
if param.Object().(*types.Var) == v {
argNo := i + 1
return b.DIVarParam(p.fn, pos, v.Name(), t, argNo)
}
}
scope := b.DIScope(p.fn, v.Parent())
return b.DIVarAuto(scope, pos, v.Name(), t)
}
@@ -970,12 +1059,22 @@ type Patches = map[string]Patch
// NewPackage compiles a Go package to LLVM IR package.
func NewPackage(prog llssa.Program, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, err error) {
ret, _, err = NewPackageEx(prog, nil, pkg, files)
ret, _, err = NewPackageEx(prog, nil, nil, pkg, files)
return
}
// NewPackageEx compiles a Go package to LLVM IR package.
func NewPackageEx(prog llssa.Program, patches Patches, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, externs []string, err error) {
//
// Parameters:
// - prog: target LLVM SSA program context
// - patches: optional package patches applied during compilation
// - rewrites: per-package string initializers rewritten at compile time
// - pkg: SSA package to compile
// - files: parsed AST files that belong to the package
//
// The rewrites map uses short variable names (without package qualifier) and
// only affects string-typed globals defined in the current package.
func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, externs []string, err error) {
pkgProg := pkg.Prog
pkgTypes := pkg.Pkg
oldTypes := pkgTypes
@@ -1008,6 +1107,7 @@ func NewPackageEx(prog llssa.Program, patches Patches, pkg *ssa.Package, files [
types.Unsafe: {kind: PkgDeclOnly}, // TODO(xsw): PkgNoInit or PkgDeclOnly?
},
cgoSymbols: make([]string, 0, 128),
rewrites: rewrites,
}
ctx.initPyModule()
ctx.initFiles(pkgPath, files, pkgName == "C")

View File

@@ -73,7 +73,7 @@ func (p *pkgSymInfo) initLinknames(ctx *context) {
lines := bytes.Split(b, sep)
for _, line := range lines {
if bytes.HasPrefix(line, commentPrefix) {
ctx.initLinkname(string(line), func(inPkgName string) (fullName string, isVar, ok bool) {
ctx.initLinkname(string(line), func(inPkgName string, isExport bool) (fullName string, isVar, ok bool) {
if sym, ok := p.syms[inPkgName]; ok && file == sym.file {
return sym.fullName, sym.isVar, true
}
@@ -277,8 +277,8 @@ func (p *context) initLinknameByDoc(doc *ast.CommentGroup, fullName, inPkgName s
if doc != nil {
for n := len(doc.List) - 1; n >= 0; n-- {
line := doc.List[n].Text
ret := p.initLinkname(line, func(name string) (_ string, _, ok bool) {
return fullName, isVar, name == inPkgName
ret := p.initLinkname(line, func(name string, isExport bool) (_ string, _, ok bool) {
return fullName, isVar, name == inPkgName || (isExport && enableExportRename)
})
if ret != unknownDirective {
return ret == hasLinkname
@@ -294,7 +294,7 @@ const (
unknownDirective = -1
)
func (p *context) initLinkname(line string, f func(inPkgName string) (fullName string, isVar, ok bool)) int {
func (p *context) initLinkname(line string, f func(inPkgName string, isExport bool) (fullName string, isVar, ok bool)) int {
const (
linkname = "//go:linkname "
llgolink = "//llgo:link "
@@ -324,17 +324,24 @@ func (p *context) initLinkname(line string, f func(inPkgName string) (fullName s
return noDirective
}
func (p *context) initLink(line string, prefix int, export bool, f func(inPkgName string) (fullName string, isVar, ok bool)) {
func (p *context) initLink(line string, prefix int, export bool, f func(inPkgName string, isExport bool) (fullName string, isVar, ok bool)) {
text := strings.TrimSpace(line[prefix:])
if idx := strings.IndexByte(text, ' '); idx > 0 {
inPkgName := text[:idx]
if fullName, _, ok := f(inPkgName); ok {
if fullName, _, ok := f(inPkgName, export); ok {
link := strings.TrimLeft(text[idx+1:], " ")
p.prog.SetLinkname(fullName, link)
if export {
p.pkg.SetExport(fullName, link)
}
} else {
// Export with different names already processed by initLinknameByDoc
if export && enableExportRename {
return
}
if export {
panic(fmt.Sprintf("export comment has wrong name %q", inPkgName))
}
fmt.Fprintln(os.Stderr, "==>", line)
fmt.Fprintf(os.Stderr, "llgo: linkname %s not found and ignored\n", inPkgName)
}
@@ -504,6 +511,7 @@ const (
llgoCgoCgocall = llgoCgoBase + 0x7
llgoAsm = llgoInstrBase + 0x40
llgoStackSave = llgoInstrBase + 0x41
llgoAtomicOpLast = llgoAtomicOpBase + int(llssa.OpUMin)
)

View File

@@ -426,6 +426,7 @@ var llgoInstrs = map[string]int{
"_cgo_runtime_cgocall": llgoCgoCgocall,
"asm": llgoAsm,
"stackSave": llgoStackSave,
}
// funcOf returns a function by name and set ftype = goFunc, cFunc, etc.
@@ -601,6 +602,8 @@ func (p *context) call(b llssa.Builder, act llssa.DoAction, call *ssa.CallCommon
ret = p.sigsetjmp(b, args)
case llgoSiglongjmp:
p.siglongjmp(b, args)
case llgoStackSave:
ret = b.StackSave()
case llgoSigjmpbuf: // func sigjmpbuf()
ret = b.AllocaSigjmpBuf()
case llgoDeferData: // func deferData() *Defer

169
cl/rewrite_internal_test.go Normal file
View File

@@ -0,0 +1,169 @@
//go:build !llgo
// +build !llgo
package cl
import (
"go/ast"
"go/parser"
"go/token"
"go/types"
"runtime"
"strings"
"testing"
gpackages "github.com/goplus/gogen/packages"
llssa "github.com/goplus/llgo/ssa"
"github.com/goplus/llgo/ssa/ssatest"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
)
func init() {
llssa.Initialize(llssa.InitAll | llssa.InitNative)
}
func compileWithRewrites(t *testing.T, src string, rewrites map[string]string) string {
t.Helper()
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "rewrite.go", src, 0)
if err != nil {
t.Fatalf("parse failed: %v", err)
}
importer := gpackages.NewImporter(fset)
mode := ssa.SanityCheckFunctions | ssa.InstantiateGenerics
pkg, _, err := ssautil.BuildPackage(&types.Config{Importer: importer}, fset,
types.NewPackage(file.Name.Name, file.Name.Name), []*ast.File{file}, mode)
if err != nil {
t.Fatalf("build package failed: %v", err)
}
prog := ssatest.NewProgramEx(t, nil, importer)
prog.TypeSizes(types.SizesFor("gc", runtime.GOARCH))
ret, _, err := NewPackageEx(prog, nil, rewrites, pkg, []*ast.File{file})
if err != nil {
t.Fatalf("NewPackageEx failed: %v", err)
}
return ret.String()
}
func TestRewriteGlobalStrings(t *testing.T) {
const src = `package rewritepkg
var VarInit = "original_value"
var VarPlain string
func Use() string { return VarInit + VarPlain }
`
ir := compileWithRewrites(t, src, map[string]string{
"VarInit": "rewrite_init",
"VarPlain": "rewrite_plain",
})
if strings.Contains(ir, "original_value") {
t.Fatalf("original initializer still present:\n%s", ir)
}
for _, want := range []string{`c"rewrite_init"`, `c"rewrite_plain"`} {
if !strings.Contains(ir, want) {
t.Fatalf("missing %s in IR:\n%s", want, ir)
}
}
}
func TestRewriteSkipsNonConstStores(t *testing.T) {
const src = `package rewritepkg
import "strings"
var VarInit = strings.ToUpper("original_value")
var VarPlain string
func Use() string { return VarInit + VarPlain }
`
ir := compileWithRewrites(t, src, map[string]string{
"VarInit": "rewrite_init",
"VarPlain": "rewrite_plain",
})
if !strings.Contains(ir, `c"rewrite_init"`) {
t.Fatalf("expected rewrite_init constant to remain:\n%s", ir)
}
if !strings.Contains(ir, "strings.ToUpper") {
t.Fatalf("expected call to strings.ToUpper in IR:\n%s", ir)
}
}
func TestRewriteValueNoDot(t *testing.T) {
ctx := &context{rewrites: map[string]string{"VarInit": "rewrite_init"}}
if _, ok := ctx.rewriteValue("VarInit"); ok {
t.Fatalf("rewriteValue should skip names without package prefix")
}
if _, ok := ctx.rewriteValue("pkg."); ok {
t.Fatalf("rewriteValue should skip trailing dot names")
}
}
func TestIsStringPtrTypeDefault(t *testing.T) {
ctx := &context{}
if ctx.isStringPtrType(types.NewPointer(types.Typ[types.Int])) {
t.Fatalf("expected non-string pointer to return false")
}
}
func TestIsStringPtrTypeBranches(t *testing.T) {
ctx := &context{}
if ctx.isStringPtrType(types.NewSlice(types.Typ[types.String])) {
t.Fatalf("slice should trigger default branch and return false")
}
if ctx.isStringPtrType(nil) {
t.Fatalf("nil type should return false")
}
if !ctx.isStringPtrType(types.NewPointer(types.Typ[types.String])) {
t.Fatalf("*string should return true")
}
}
func TestRewriteIgnoredInNonInitStore(t *testing.T) {
const src = `package rewritepkg
var VarInit = "original_value"
func Override() { VarInit = "override_value" }
`
ir := compileWithRewrites(t, src, map[string]string{"VarInit": "rewrite_init"})
if !strings.Contains(ir, `c"override_value"`) {
t.Fatalf("override store should retain original literal:\n%s", ir)
}
if !strings.Contains(ir, `c"rewrite_init"`) {
t.Fatalf("global initializer should still be rewritten:\n%s", ir)
}
}
func TestRewriteMissingEntry(t *testing.T) {
const src = `package rewritepkg
var VarInit = "original_value"
var VarOther = "other_value"
`
ir := compileWithRewrites(t, src, map[string]string{"VarInit": "rewrite_init"})
if !strings.Contains(ir, `c"other_value"`) {
t.Fatalf("VarOther should keep original initializer:\n%s", ir)
}
if !strings.Contains(ir, `c"rewrite_init"`) {
t.Fatalf("VarInit should still be rewritten:\n%s", ir)
}
}
func TestRewriteIgnoresNonStringVar(t *testing.T) {
const src = `package rewritepkg
type wrapper struct{ v int }
var VarStruct = wrapper{v: 1}
`
ir := compileWithRewrites(t, src, map[string]string{"VarStruct": "rewrite_struct"})
if strings.Contains(ir, `c"rewrite_struct"`) {
t.Fatalf("non-string variables must not be rewritten:\n%s", ir)
}
}
func TestRewriteIgnoresStringAlias(t *testing.T) {
const src = `package rewritepkg
type T string
var VarAlias T = "original_value"
`
ir := compileWithRewrites(t, src, map[string]string{"VarAlias": "rewrite_alias"})
if strings.Contains(ir, `c"rewrite_alias"`) {
t.Fatalf("string alias types must not be rewritten:\n%s", ir)
}
if !strings.Contains(ir, `c"original_value"`) {
t.Fatalf("original value should remain for alias type:\n%s", ir)
}
}

View File

@@ -36,6 +36,9 @@ var CheckLinkArgs bool
var CheckLLFiles bool
var GenLLFiles bool
var ForceEspClang bool
var SizeReport bool
var SizeFormat string
var SizeLevel string
func AddCommonFlags(fs *flag.FlagSet) {
fs.BoolVar(&Verbose, "v", false, "Verbose output")
@@ -51,6 +54,10 @@ func AddBuildFlags(fs *flag.FlagSet) {
fs.BoolVar(&GenLLFiles, "gen-llfiles", false, "generate .ll files for pkg export")
fs.BoolVar(&ForceEspClang, "force-espclang", false, "force to use esp-clang")
}
fs.BoolVar(&SizeReport, "size", false, "Print size report after build")
fs.StringVar(&SizeFormat, "size:format", "", "Size report format (text,json)")
fs.StringVar(&SizeLevel, "size:level", "", "Size report aggregation level (full,module,package)")
}
func AddBuildModeFlags(fs *flag.FlagSet) {
@@ -79,6 +86,15 @@ func UpdateConfig(conf *build.Config) error {
conf.Target = Target
conf.Port = Port
conf.BaudRate = BaudRate
if SizeReport || SizeFormat != "" || SizeLevel != "" {
conf.SizeReport = true
if SizeFormat != "" {
conf.SizeFormat = SizeFormat
}
if SizeLevel != "" {
conf.SizeLevel = SizeLevel
}
}
switch conf.Mode {
case build.ModeBuild:

32
doc/defer-tls-gc.md Normal file
View File

@@ -0,0 +1,32 @@
# Defer Loop GC Integration
## Background
`defer` chains are stored in a per-thread TLS slot so that unwind paths can locate the active `*runtime.Defer`. With the default allocator (`AllocU`) backed by Boehm GC (bdwgc), those TLS-resident pointers were invisible to the collector. In stress scenarios—e.g. `TestDeferLoopStress` with 1,000,000 defers—the collector reclaimed the defer nodes, leaving dangling pointers and causing crashes inside the deferred closures.
Prior experiments (`test-defer-dont-free` branch) confirmed the crash disappeared when allocations bypassed GC (plain `malloc` without `free`), pointing to a root-registration gap rather than logical corruption.
## Solution Overview
1. **GC-aware TLS slot helper** *(from PR [#1347](https://github.com/goplus/llgo/pull/1347))*
- Added `runtime/internal/clite/tls`, which exposes `tls.Alloc` to create per-thread storage that is automatically registered as a Boehm GC root.
- `SetThreadDefer` delegates to this helper so every thread reuses the same GC-safe slot without bespoke plumbing.
- The package handles TLS key creation, root registration/removal, and invokes an optional destructor when a thread exits.
2. **SSA codegen synchronization**
- `ssa/eh.go` now calls `runtime.SetThreadDefer` whenever it updates the TLS pointer (on first allocation and when restoring the previous link during unwind).
- Defer argument nodes and the `runtime.Defer` struct itself are allocated with `aggregateAllocU`, ensuring new memory comes from GC-managed heaps, and nodes are released via `runtime.FreeDeferNode`.
3. **Non-GC builds**
- The `tls` helper falls back to a malloc-backed TLS slot without GC registration, while `FreeDeferNode` continues to release nodes via `c.Free` when building with `-tags nogc`.
## Testing
Run the stress and regression suites to validate the integration:
```sh
./llgo.sh test ./test -run TestDeferLoopStress
./llgo.sh test ./test
```
The updated `TestDeferLoopStress` now asserts 1,000,000 loop defers execute without failure, catching regressions in GC root tracking.

58
doc/size-report.md Normal file
View File

@@ -0,0 +1,58 @@
# Size Report Options
The `llgo build -size` flag emits a TinyGo-style table showing how much code,
rodata, data, and BSS each component contributes to the final binary. This
document captures the parsing strategy and new aggregation controls.
## Parsing Strategy
- We invoke `llvm-readelf --elf-output-style=LLVM --all <binary>` and parse the textual output with an
indentation-sensitive state machine (no JSON). Only the `Sections` and
`Symbols` blocks are inspected.
- Section metadata records the index, address, size, name, and segment. Each
section is classified into text/rodata/data/bss buckets.
- Symbols are attached to their containing sections with start addresses. By
sorting symbols and walking their ranges, we compute byte spans that can be
attributed to packages/modules.
- Sections with no symbols fall back to `(unknown <section>)`, and gaps become
`(padding <section>)` entries so totals still add up.
## Aggregation Levels
`-size:level` controls how symbol names are grouped prior to reporting:
| Level | Behavior |
|-----------|---------------------------------------------------------------------------|
| `full` | Keeps the raw owner from the symbol name (previous behavior). |
| `package` | Uses the list of packages built in `build.Do` and groups by `pkg.PkgPath`. |
| `module`* | Default. Groups by `pkg.Module.Path` (or `pkg.PkgPath` if the module is nil). |
Matching is performed by checking whether the demangled symbol name begins with
`pkg.PkgPath + "."`. Symbols that do not match any package and contain `llgo` are
bucketed into `llgo-stubs`; other unmatched entries keep their original owner
names so we can inspect them later.
Examples:
```sh
llgo build -size . # module-level aggregation (default)
llgo build -size -size:level=package . # collapse by package ID
llgo build -size -size:level=full . # show raw symbol owners
llgo build -size -size:format=json . # JSON output (works with all levels)
```
## Validation
1. Unit tests: `go test ./internal/build -run TestParseReadelfOutput -count=1`.
2. Real binary test:
```sh
cd cl/_testgo/rewrite
../../../llgo.sh build .
LLGO_SIZE_REPORT_BIN=$(pwd)/rewrite \
go test ./internal/build -run TestParseReadelfRealBinary -count=1
```
3. Manual smoke test: `../../../llgo.sh build -size -size:level=module .` (or
`package`/`full` as desired).
The parser works across Mach-O and ELF targets as long as `llvm-readelf` is in
`PATH`.

4
go.mod
View File

@@ -6,8 +6,8 @@ toolchain go1.24.1
require (
github.com/goplus/cobra v1.9.12 //gop:class
github.com/goplus/gogen v1.19.3
github.com/goplus/lib v0.3.0
github.com/goplus/gogen v1.19.5
github.com/goplus/lib v0.3.1
github.com/goplus/llgo/runtime v0.0.0-00010101000000-000000000000
github.com/goplus/llvm v0.8.5
github.com/goplus/mod v0.17.1

8
go.sum
View File

@@ -6,10 +6,10 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/goplus/cobra v1.9.12 h1:0F9EdEbeGyITGz+mqoHoJ5KpUw97p1CkxV74IexHw5s=
github.com/goplus/cobra v1.9.12/go.mod h1:p4LhfNJDKEpiGjGiNn0crUXL5dUPA5DX2ztYpEJR34E=
github.com/goplus/gogen v1.19.3 h1:sMTe7xME8lWFdPL6NcULykdJtFs9CtXkNACRbaAKTiQ=
github.com/goplus/gogen v1.19.3/go.mod h1:owX2e1EyU5WD+Nm6oH2m/GXjLdlBYcwkLO4wN8HHXZI=
github.com/goplus/lib v0.3.0 h1:y0ZGb5Q/RikW1oMMB4Di7XIZIpuzh/7mlrR8HNbxXCA=
github.com/goplus/lib v0.3.0/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0=
github.com/goplus/gogen v1.19.5 h1:YWPwpRA1PusPhptv9jKg/XiN+AQGiAD9r6I86mJ3lR4=
github.com/goplus/gogen v1.19.5/go.mod h1:owX2e1EyU5WD+Nm6oH2m/GXjLdlBYcwkLO4wN8HHXZI=
github.com/goplus/lib v0.3.1 h1:Xws4DBVvgOMu58awqB972wtvTacDbk3nqcbHjdx9KSg=
github.com/goplus/lib v0.3.1/go.mod h1:SgJv3oPqLLHCu0gcL46ejOP3x7/2ry2Jtxu7ta32kp0=
github.com/goplus/llvm v0.8.5 h1:DUnFeYC3Rco622tBEKGg8xkigRAV2fh5ZIfBCt7gOSs=
github.com/goplus/llvm v0.8.5/go.mod h1:PeVK8GgzxwAYCiMiUAJb5wJR6xbhj989tu9oulKLLT4=
github.com/goplus/mod v0.17.1 h1:ITovxDcc5zbURV/Wrp3/SBsYLgC1KrxY6pq1zMM2V94=

View File

@@ -132,10 +132,19 @@ type Config struct {
CheckLinkArgs bool // check linkargs valid
ForceEspClang bool // force to use esp-clang
Tags string
GlobalNames map[string][]string // pkg => names
GlobalDatas map[string]string // pkg.name => data
SizeReport bool // print size report after successful build
SizeFormat string // size report format: text,json
SizeLevel string // size aggregation level: full,module,package
// GlobalRewrites specifies compile-time overrides for global string variables.
// Keys are fully qualified package paths (e.g. "main" or "github.com/user/pkg").
// Each Rewrites entry maps variable names to replacement string values. Only
// string-typed globals are supported and "main" applies to all root main
// packages in the current build.
GlobalRewrites map[string]Rewrites
}
type Rewrites map[string]string
func NewDefaultConf(mode Mode) *Config {
bin := os.Getenv("GOBIN")
if bin == "" {
@@ -199,6 +208,15 @@ func Do(args []string, conf *Config) ([]Package, error) {
if conf.BuildMode == "" {
conf.BuildMode = BuildModeExe
}
if conf.SizeReport && conf.SizeFormat == "" {
conf.SizeFormat = "text"
}
if conf.SizeReport && conf.SizeLevel == "" {
conf.SizeLevel = "module"
}
if err := ensureSizeReporting(conf); err != nil {
return nil, err
}
// Handle crosscompile configuration first to set correct GOOS/GOARCH
forceEspClang := conf.ForceEspClang || conf.Target != ""
export, err := crosscompile.Use(conf.Goos, conf.Goarch, conf.Target, IsWasiThreadsEnabled(), forceEspClang)
@@ -214,6 +232,11 @@ func Do(args []string, conf *Config) ([]Package, error) {
conf.Goarch = export.GOARCH
}
// Enable different export names for TinyGo compatibility when using -target
if conf.Target != "" {
cl.EnableExportRename(true)
}
verbose := conf.Verbose
patterns := args
tags := "llgo,math_big_pure_go"
@@ -335,6 +358,10 @@ func Do(args []string, conf *Config) ([]Package, error) {
crossCompile: export,
cTransformer: cabi.NewTransformer(prog, export.LLVMTarget, export.TargetABI, conf.AbiMode, cabiOptimize),
}
// default runtime globals must be registered before packages are built
addGlobalString(conf, "runtime.defaultGOROOT="+runtime.GOROOT(), nil)
addGlobalString(conf, "runtime.buildVersion="+runtime.Version(), nil)
pkgs, err := buildAllPkgs(ctx, initial, verbose)
check(err)
if mode == ModeGen {
@@ -345,19 +372,11 @@ func Do(args []string, conf *Config) ([]Package, error) {
}
return nil, fmt.Errorf("initial package not found")
}
dpkg, err := buildAllPkgs(ctx, altPkgs[noRt:], verbose)
check(err)
allPkgs := append([]*aPackage{}, pkgs...)
allPkgs = append(allPkgs, dpkg...)
// update globals importpath.name=value
addGlobalString(conf, "runtime.defaultGOROOT="+runtime.GOROOT(), nil)
addGlobalString(conf, "runtime.buildVersion="+runtime.Version(), nil)
global, err := createGlobals(ctx, ctx.prog, pkgs)
check(err)
for _, pkg := range initial {
if needLink(pkg, mode) {
name := path.Base(pkg.PkgPath)
@@ -369,10 +388,15 @@ func Do(args []string, conf *Config) ([]Package, error) {
}
// Link main package using the output path from buildOutFmts
err = linkMainPkg(ctx, pkg, allPkgs, global, outFmts.Out, verbose)
err = linkMainPkg(ctx, pkg, allPkgs, outFmts.Out, verbose)
if err != nil {
return nil, err
}
if conf.Mode == ModeBuild && conf.SizeReport {
if err := reportBinarySize(outFmts.Out, conf.SizeFormat, conf.SizeLevel, allPkgs); err != nil {
return nil, err
}
}
// Generate C headers for c-archive and c-shared modes before linking
if ctx.buildConf.BuildMode == BuildModeCArchive || ctx.buildConf.BuildMode == BuildModeCShared {
@@ -629,58 +653,59 @@ var (
errXflags = errors.New("-X flag requires argument of the form importpath.name=value")
)
// maxRewriteValueLength limits the size of rewrite values to prevent
// excessive memory usage and potential resource exhaustion during compilation.
const maxRewriteValueLength = 1 << 20 // 1 MiB cap per rewrite value
func addGlobalString(conf *Config, arg string, mainPkgs []string) {
addGlobalStringWith(conf, arg, mainPkgs, true)
}
func addGlobalStringWith(conf *Config, arg string, mainPkgs []string, skipIfExists bool) {
eq := strings.Index(arg, "=")
dot := strings.LastIndex(arg[:eq+1], ".")
if eq < 0 || dot < 0 {
panic(errXflags)
}
pkg := arg[:dot]
varName := arg[dot+1 : eq]
value := arg[eq+1:]
validateRewriteInput(pkg, varName, value)
pkgs := []string{pkg}
if pkg == "main" {
pkgs = mainPkgs
}
if conf.GlobalNames == nil {
conf.GlobalNames = make(map[string][]string)
if len(pkgs) == 0 {
return
}
if conf.GlobalDatas == nil {
conf.GlobalDatas = make(map[string]string)
if conf.GlobalRewrites == nil {
conf.GlobalRewrites = make(map[string]Rewrites)
}
for _, pkg := range pkgs {
name := pkg + arg[dot:eq]
value := arg[eq+1:]
if _, ok := conf.GlobalDatas[name]; !ok {
conf.GlobalNames[pkg] = append(conf.GlobalNames[pkg], name)
for _, realPkg := range pkgs {
vars := conf.GlobalRewrites[realPkg]
if vars == nil {
vars = make(Rewrites)
conf.GlobalRewrites[realPkg] = vars
}
conf.GlobalDatas[name] = value
if skipIfExists {
if _, exists := vars[varName]; exists {
continue
}
}
vars[varName] = value
}
}
func createGlobals(ctx *context, prog llssa.Program, pkgs []*aPackage) (llssa.Package, error) {
if len(ctx.buildConf.GlobalDatas) == 0 {
return nil, nil
func validateRewriteInput(pkg, varName, value string) {
if pkg == "" || strings.ContainsAny(pkg, " \t\r\n") {
panic(fmt.Errorf("invalid package path for rewrite: %q", pkg))
}
for _, pkg := range pkgs {
if pkg.ExportFile == "" {
continue
if !token.IsIdentifier(varName) {
panic(fmt.Errorf("invalid variable name for rewrite: %q", varName))
}
if names, ok := ctx.buildConf.GlobalNames[pkg.PkgPath]; ok {
err := pkg.LPkg.Undefined(names...)
if err != nil {
return nil, err
if len(value) > maxRewriteValueLength {
panic(fmt.Errorf("rewrite value too large: %d bytes", len(value)))
}
pkg.ExportFile += "-global"
pkg.ExportFile, err = exportObject(ctx, pkg.PkgPath+".global", pkg.ExportFile, []byte(pkg.LPkg.String()))
if err != nil {
return nil, err
}
}
}
global := prog.NewPackage("", "global")
for name, value := range ctx.buildConf.GlobalDatas {
global.AddGlobalString(name, value)
}
return global, nil
}
// compileExtraFiles compiles extra files (.s/.c) from target configuration and returns object files
@@ -752,7 +777,7 @@ func compileExtraFiles(ctx *context, verbose bool) ([]string, error) {
return objFiles, nil
}
func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global llssa.Package, outputPath string, verbose bool) error {
func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPath string, verbose bool) error {
needRuntime := false
needPyInit := false
@@ -769,7 +794,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
if p.ExportFile != "" && aPkg != nil { // skip packages that only contain declarations
linkArgs = append(linkArgs, aPkg.LinkArgs...)
objFiles = append(objFiles, aPkg.LLFiles...)
objFiles = append(objFiles, aPkg.ExportFile)
need1, need2 := isNeedRuntimeOrPyInit(ctx, p)
if !needRuntime {
needRuntime = need1
@@ -780,11 +804,11 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
}
})
// Generate main module file (needed for global variables even in library modes)
entryObjFile, err := genMainModuleFile(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit)
entryPkg := genMainModule(ctx, llssa.PkgRuntime, pkg, needRuntime, needPyInit)
entryObjFile, err := exportObject(ctx, entryPkg.PkgPath, entryPkg.ExportFile, []byte(entryPkg.LPkg.String()))
if err != nil {
return err
}
// defer os.Remove(entryLLFile)
objFiles = append(objFiles, entryObjFile)
// Compile extra files from target configuration
@@ -794,14 +818,6 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, global l
}
objFiles = append(objFiles, extraObjFiles...)
if global != nil {
export, err := exportObject(ctx, pkg.PkgPath+".global", pkg.ExportFile+"-global", []byte(global.String()))
if err != nil {
return err
}
objFiles = append(objFiles, export)
}
if IsFullRpathEnabled() {
// Treat every link-time library search path, specified by the -L parameter, as a runtime search path as well.
// This is to ensure the final executable can locate libraries with a relocatable install_name
@@ -914,118 +930,6 @@ func needStart(ctx *context) bool {
}
}
func genMainModuleFile(ctx *context, rtPkgPath string, pkg *packages.Package, needRuntime, needPyInit bool) (path string, err error) {
var (
pyInitDecl string
pyInit string
rtInitDecl string
rtInit string
)
mainPkgPath := pkg.PkgPath
if needRuntime {
rtInit = "call void @\"" + rtPkgPath + ".init\"()"
rtInitDecl = "declare void @\"" + rtPkgPath + ".init\"()"
}
if needPyInit {
pyInit = "call void @Py_Initialize()"
pyInitDecl = "declare void @Py_Initialize()"
}
declSizeT := "%size_t = type i64"
if is32Bits(ctx.buildConf.Goarch) {
declSizeT = "%size_t = type i32"
}
stdioDecl := ""
stdioNobuf := ""
if IsStdioNobuf() {
stdioDecl = `
@stdout = external global ptr
@stderr = external global ptr
@__stdout = external global ptr
@__stderr = external global ptr
declare i32 @setvbuf(ptr, ptr, i32, %size_t)
`
stdioNobuf = `
; Set stdout with no buffer
%stdout_is_null = icmp eq ptr @stdout, null
%stdout_ptr = select i1 %stdout_is_null, ptr @__stdout, ptr @stdout
call i32 @setvbuf(ptr %stdout_ptr, ptr null, i32 2, %size_t 0)
; Set stderr with no buffer
%stderr_ptr = select i1 %stdout_is_null, ptr @__stderr, ptr @stderr
call i32 @setvbuf(ptr %stderr_ptr, ptr null, i32 2, %size_t 0)
`
}
// TODO(lijie): workaround for libc-free
// Remove main/_start when -buildmode and libc are ready
startDefine := `
define weak void @_start() {
; argc = 0
%argc = add i32 0, 0
; argv = null
%argv = inttoptr i64 0 to i8**
call i32 @main(i32 %argc, i8** %argv)
ret void
}
`
mainDefine := "define i32 @main(i32 noundef %0, ptr nocapture noundef readnone %1) local_unnamed_addr"
if !needStart(ctx) && isWasmTarget(ctx.buildConf.Goos) {
mainDefine = "define hidden noundef i32 @__main_argc_argv(i32 noundef %0, ptr nocapture noundef readnone %1) local_unnamed_addr"
}
if !needStart(ctx) {
startDefine = ""
}
var mainCode string
// For library modes (c-archive, c-shared), only generate global variables
if ctx.buildConf.BuildMode != BuildModeExe {
mainCode = `; ModuleID = 'main'
source_filename = "main"
@__llgo_argc = global i32 0, align 4
@__llgo_argv = global ptr null, align 8
`
} else {
// For executable mode, generate full main function
mainCode = fmt.Sprintf(`; ModuleID = 'main'
source_filename = "main"
%s
@__llgo_argc = global i32 0, align 4
@__llgo_argv = global ptr null, align 8
%s
%s
%s
declare void @"%s.init"()
declare void @"%s.main"()
define weak void @runtime.init() {
ret void
}
; TODO(lijie): workaround for syscall patch
define weak void @"syscall.init"() {
ret void
}
%s
%s {
_llgo_0:
store i32 %%0, ptr @__llgo_argc, align 4
store ptr %%1, ptr @__llgo_argv, align 8
%s
%s
%s
call void @runtime.init()
call void @"%s.init"()
call void @"%s.main"()
ret i32 0
}
`, declSizeT, stdioDecl,
pyInitDecl, rtInitDecl, mainPkgPath, mainPkgPath,
startDefine, mainDefine, stdioNobuf,
pyInit, rtInit, mainPkgPath, mainPkgPath)
}
return exportObject(ctx, pkg.PkgPath+".main", pkg.ExportFile+"-main", []byte(mainCode))
}
func is32Bits(goarch string) bool {
return goarch == "386" || goarch == "arm" || goarch == "mips" || goarch == "wasm"
}
@@ -1050,7 +954,7 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error {
cl.SetDebug(cl.DbgFlagAll)
}
ret, externs, err := cl.NewPackageEx(ctx.prog, ctx.patches, aPkg.SSA, syntax)
ret, externs, err := cl.NewPackageEx(ctx.prog, ctx.patches, aPkg.rewriteVars, aPkg.SSA, syntax)
if showDetail {
llssa.SetDebug(0)
cl.SetDebug(0)
@@ -1077,10 +981,11 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error {
aPkg.LinkArgs = append(aPkg.LinkArgs, altLdflags...)
}
if pkg.ExportFile != "" {
pkg.ExportFile, err = exportObject(ctx, pkg.PkgPath, pkg.ExportFile, []byte(ret.String()))
exportFile, err := exportObject(ctx, pkg.PkgPath, pkg.ExportFile, []byte(ret.String()))
if err != nil {
return fmt.Errorf("export object of %v failed: %v", pkgPath, err)
}
aPkg.LLFiles = append(aPkg.LLFiles, exportFile)
if debugBuild || verbose {
fmt.Fprintf(os.Stderr, "==> Export %s: %s\n", aPkg.PkgPath, pkg.ExportFile)
}
@@ -1089,11 +994,15 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error {
}
func exportObject(ctx *context, pkgPath string, exportFile string, data []byte) (string, error) {
f, err := os.CreateTemp("", "llgo-*.ll")
base := filepath.Base(exportFile)
f, err := os.CreateTemp("", base+"-*.ll")
if err != nil {
return "", err
}
f.Write(data)
if _, err := f.Write(data); err != nil {
f.Close()
return "", err
}
err = f.Close()
if err != nil {
return exportFile, err
@@ -1111,13 +1020,17 @@ func exportObject(ctx *context, pkgPath string, exportFile string, data []byte)
}
return exportFile, os.Rename(f.Name(), exportFile)
}
exportFile += ".o"
args := []string{"-o", exportFile, "-c", f.Name(), "-Wno-override-module"}
objFile, err := os.CreateTemp("", base+"-*.o")
if err != nil {
return "", err
}
objFile.Close()
args := []string{"-o", objFile.Name(), "-c", f.Name(), "-Wno-override-module"}
if ctx.buildConf.Verbose {
fmt.Fprintln(os.Stderr, "clang", args)
}
cmd := ctx.compiler()
return exportFile, cmd.Compile(args...)
return objFile.Name(), cmd.Compile(args...)
}
func llcCheck(env *llvm.Env, exportFile string) (msg string, err error) {
@@ -1173,6 +1086,7 @@ type aPackage struct {
LinkArgs []string
LLFiles []string
rewriteVars map[string]string
}
type Package = *aPackage
@@ -1193,7 +1107,8 @@ func allPkgs(ctx *context, initial []*packages.Package, verbose bool) (all []*aP
return
}
}
all = append(all, &aPackage{p, ssaPkg, altPkg, nil, nil, nil})
rewrites := collectRewriteVars(ctx, pkgPath)
all = append(all, &aPackage{p, ssaPkg, altPkg, nil, nil, nil, rewrites})
} else {
errs = append(errs, p)
}
@@ -1201,6 +1116,32 @@ func allPkgs(ctx *context, initial []*packages.Package, verbose bool) (all []*aP
return
}
func collectRewriteVars(ctx *context, pkgPath string) map[string]string {
data := ctx.buildConf.GlobalRewrites
if len(data) == 0 {
return nil
}
basePath := strings.TrimPrefix(pkgPath, altPkgPathPrefix)
if vars := data[basePath]; vars != nil {
return cloneRewrites(vars)
}
if vars := data[pkgPath]; vars != nil {
return cloneRewrites(vars)
}
return nil
}
func cloneRewrites(src Rewrites) map[string]string {
if len(src) == 0 {
return nil
}
dup := make(map[string]string, len(src))
for k, v := range src {
dup[k] = v
}
return dup
}
func createSSAPkg(prog *ssa.Program, p *packages.Package, verbose bool) *ssa.Package {
pkgSSA := prog.ImportedPackage(p.ID)
if pkgSSA == nil {

View File

@@ -8,6 +8,9 @@ import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"
"github.com/goplus/llgo/internal/mockable"
@@ -94,4 +97,70 @@ func TestCmpTest(t *testing.T) {
mockRun([]string{"../../cl/_testgo/runtest"}, &Config{Mode: ModeCmpTest})
}
// TestGenerateOutputFilenames removed - functionality moved to filename_test.go
const (
rewriteMainPkg = "github.com/goplus/llgo/cl/_testgo/rewrite"
rewriteDepPkg = rewriteMainPkg + "/dep"
rewriteDirPath = "../../cl/_testgo/rewrite"
)
func TestLdFlagsRewriteVars(t *testing.T) {
buildRewriteBinary(t, false, "build-main", "build-pkg")
buildRewriteBinary(t, false, "rerun-main", "rerun-pkg")
}
func TestLdFlagsRewriteVarsMainAlias(t *testing.T) {
buildRewriteBinary(t, true, "alias-main", "alias-pkg")
}
func buildRewriteBinary(t *testing.T, useMainAlias bool, mainVal, depVal string) {
t.Helper()
binPath := filepath.Join(t.TempDir(), "rewrite")
if runtime.GOOS == "windows" {
binPath += ".exe"
}
cfg := &Config{Mode: ModeBuild, OutFile: binPath}
mainKey := rewriteMainPkg
var mainPkgs []string
if useMainAlias {
mainKey = "main"
mainPkgs = []string{rewriteMainPkg}
}
mainPlain := mainVal + "-plain"
depPlain := depVal + "-plain"
gorootVal := "goroot-" + mainVal
versionVal := "version-" + mainVal
addGlobalString(cfg, mainKey+".VarName="+mainVal, mainPkgs)
addGlobalString(cfg, mainKey+".VarPlain="+mainPlain, mainPkgs)
addGlobalString(cfg, rewriteDepPkg+".VarName="+depVal, nil)
addGlobalString(cfg, rewriteDepPkg+".VarPlain="+depPlain, nil)
addGlobalString(cfg, "runtime.defaultGOROOT="+gorootVal, nil)
addGlobalString(cfg, "runtime.buildVersion="+versionVal, nil)
if _, err := Do([]string{rewriteDirPath}, cfg); err != nil {
t.Fatalf("ModeBuild failed: %v", err)
}
got := runBinary(t, binPath)
want := fmt.Sprintf(
"main.VarName: %s\nmain.VarPlain: %s\ndep.VarName: %s\ndep.VarPlain: %s\nruntime.GOROOT(): %s\nruntime.Version(): %s\n",
mainVal, mainPlain, depVal, depPlain, gorootVal, versionVal,
)
if got != want {
t.Fatalf("unexpected binary output:\nwant %q\ngot %q", want, got)
}
}
func runBinary(t *testing.T, path string, args ...string) string {
t.Helper()
cmd := exec.Command(path, args...)
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("failed to run %s: %v\n%s", path, err, output)
}
return string(output)
}
func TestRunPrintfWithStdioNobuf(t *testing.T) {
t.Setenv(llgoStdioNobuf, "1")
mockRun([]string{"../../cl/_testdata/printf"}, &Config{Mode: ModeRun})
}

View File

@@ -0,0 +1,231 @@
//go:build !llgo
// +build !llgo
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package build contains the llgo compiler build orchestration logic.
//
// The main_module.go file generates the entry point module for llgo programs,
// which contains the main() function, initialization sequence, and global
// variables like argc/argv. This module is generated differently depending on
// BuildMode (exe, c-archive, c-shared).
package build
import (
"go/token"
"go/types"
"github.com/goplus/llgo/internal/packages"
llvm "github.com/goplus/llvm"
llssa "github.com/goplus/llgo/ssa"
)
// genMainModule generates the main entry module for an llgo program.
//
// The module contains argc/argv globals and, for executable build modes,
// the entry function that wires initialization and main. For C archive or
// shared library modes, only the globals are emitted.
func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, needRuntime, needPyInit bool) Package {
prog := ctx.prog
mainPkg := prog.NewPackage("", pkg.ID+".main")
argcVar := mainPkg.NewVarEx("__llgo_argc", prog.Pointer(prog.Int32()))
argcVar.Init(prog.Zero(prog.Int32()))
argvValueType := prog.Pointer(prog.CStr())
argvVar := mainPkg.NewVarEx("__llgo_argv", prog.Pointer(argvValueType))
argvVar.InitNil()
exportFile := pkg.ExportFile
if exportFile == "" {
exportFile = pkg.PkgPath
}
mainAPkg := &aPackage{
Package: &packages.Package{
PkgPath: pkg.PkgPath + ".main",
ExportFile: exportFile + "-main",
},
LPkg: mainPkg,
}
if ctx.buildConf.BuildMode != BuildModeExe {
return mainAPkg
}
runtimeStub := defineWeakNoArgStub(mainPkg, "runtime.init")
// TODO(lijie): workaround for syscall patch
defineWeakNoArgStub(mainPkg, "syscall.init")
var pyInit llssa.Function
if needPyInit {
pyInit = declareNoArgFunc(mainPkg, "Py_Initialize")
}
var rtInit llssa.Function
if needRuntime {
rtInit = declareNoArgFunc(mainPkg, rtPkgPath+".init")
}
mainInit := declareNoArgFunc(mainPkg, pkg.PkgPath+".init")
mainMain := declareNoArgFunc(mainPkg, pkg.PkgPath+".main")
entryFn := defineEntryFunction(ctx, mainPkg, argcVar, argvVar, argvValueType, runtimeStub, mainInit, mainMain, pyInit, rtInit)
if needStart(ctx) {
defineStart(mainPkg, entryFn, argvValueType)
}
return mainAPkg
}
// defineEntryFunction creates the program's entry function. The name is
// "main" for standard targets, or "__main_argc_argv" with hidden visibility
// for WASM targets that don't require _start.
//
// The entry stores argc/argv, optionally disables stdio buffering, runs
// initialization hooks (Python, runtime, package init), and finally calls
// main.main before returning 0.
func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa.Global, argvType llssa.Type, runtimeStub, mainInit, mainMain llssa.Function, pyInit, rtInit llssa.Function) llssa.Function {
prog := pkg.Prog
entryName := "main"
if !needStart(ctx) && isWasmTarget(ctx.buildConf.Goos) {
entryName = "__main_argc_argv"
}
sig := newEntrySignature(argvType.RawType())
fn := pkg.NewFunc(entryName, sig, llssa.InC)
fnVal := pkg.Module().NamedFunction(entryName)
if entryName != "main" {
fnVal.SetVisibility(llvm.HiddenVisibility)
fnVal.SetUnnamedAddr(true)
}
b := fn.MakeBody(1)
b.Store(argcVar.Expr, fn.Param(0))
b.Store(argvVar.Expr, fn.Param(1))
if IsStdioNobuf() {
emitStdioNobuf(b, pkg, ctx.buildConf.Goos)
}
if pyInit != nil {
b.Call(pyInit.Expr)
}
if rtInit != nil {
b.Call(rtInit.Expr)
}
b.Call(runtimeStub.Expr)
b.Call(mainInit.Expr)
b.Call(mainMain.Expr)
b.Return(prog.IntVal(0, prog.Int32()))
return fn
}
func defineStart(pkg llssa.Package, entry llssa.Function, argvType llssa.Type) {
fn := pkg.NewFunc("_start", llssa.NoArgsNoRet, llssa.InC)
pkg.Module().NamedFunction("_start").SetLinkage(llvm.WeakAnyLinkage)
b := fn.MakeBody(1)
prog := pkg.Prog
b.Call(entry.Expr, prog.IntVal(0, prog.Int32()), prog.Nil(argvType))
b.Return()
}
func declareNoArgFunc(pkg llssa.Package, name string) llssa.Function {
return pkg.NewFunc(name, llssa.NoArgsNoRet, llssa.InC)
}
func defineWeakNoArgStub(pkg llssa.Package, name string) llssa.Function {
fn := pkg.NewFunc(name, llssa.NoArgsNoRet, llssa.InC)
pkg.Module().NamedFunction(name).SetLinkage(llvm.WeakAnyLinkage)
b := fn.MakeBody(1)
b.Return()
return fn
}
const (
// ioNoBuf represents the _IONBF flag for setvbuf (no buffering)
ioNoBuf = 2
)
// emitStdioNobuf generates code to disable buffering on stdout and stderr
// when the LLGO_STDIO_NOBUF environment variable is set. Only Darwin uses
// the alternate `__stdoutp`/`__stderrp` symbols; other targets rely on the
// standard `stdout`/`stderr` globals.
func emitStdioNobuf(b llssa.Builder, pkg llssa.Package, goos string) {
prog := pkg.Prog
streamType := prog.VoidPtr()
streamPtrType := prog.Pointer(streamType)
stdoutName := "stdout"
stderrName := "stderr"
if goos == "darwin" {
stdoutName = "__stdoutp"
stderrName = "__stderrp"
}
stdout := declareExternalPtrGlobal(pkg, stdoutName, streamPtrType)
stderr := declareExternalPtrGlobal(pkg, stderrName, streamPtrType)
stdoutPtr := b.Load(stdout)
stderrPtr := b.Load(stderr)
sizeType := prog.Uintptr()
setvbuf := declareSetvbuf(pkg, streamPtrType, prog.CStr(), prog.Int32(), sizeType)
noBufMode := prog.IntVal(ioNoBuf, prog.Int32())
zeroSize := prog.Zero(sizeType)
nullBuf := prog.Nil(prog.CStr())
b.Call(setvbuf.Expr, stdoutPtr, nullBuf, noBufMode, zeroSize)
b.Call(setvbuf.Expr, stderrPtr, nullBuf, noBufMode, zeroSize)
}
func declareExternalPtrGlobal(pkg llssa.Package, name string, valueType llssa.Type) llssa.Expr {
global := pkg.NewVarEx(name, valueType)
pkg.Module().NamedGlobal(name).SetLinkage(llvm.ExternalLinkage)
return global.Expr
}
func declareSetvbuf(pkg llssa.Package, streamPtrType, bufPtrType, intType, sizeType llssa.Type) llssa.Function {
sig := newSignature(
[]types.Type{
streamPtrType.RawType(),
bufPtrType.RawType(),
intType.RawType(),
sizeType.RawType(),
},
[]types.Type{intType.RawType()},
)
return pkg.NewFunc("setvbuf", sig, llssa.InC)
}
func tupleOf(tys ...types.Type) *types.Tuple {
if len(tys) == 0 {
return types.NewTuple()
}
vars := make([]*types.Var, len(tys))
for i, t := range tys {
vars[i] = types.NewParam(token.NoPos, nil, "", t)
}
return types.NewTuple(vars...)
}
func newSignature(params []types.Type, results []types.Type) *types.Signature {
return types.NewSignatureType(nil, nil, nil, tupleOf(params...), tupleOf(results...), false)
}
func newEntrySignature(argvType types.Type) *types.Signature {
return newSignature(
[]types.Type{types.Typ[types.Int32], argvType},
[]types.Type{types.Typ[types.Int32]},
)
}

View File

@@ -0,0 +1,70 @@
//go:build !llgo
// +build !llgo
package build
import (
"strings"
"testing"
"github.com/goplus/llvm"
"github.com/goplus/llgo/internal/packages"
llssa "github.com/goplus/llgo/ssa"
)
func init() {
llssa.Initialize(llssa.InitAll)
}
func TestGenMainModuleExecutable(t *testing.T) {
llvm.InitializeAllTargets()
t.Setenv(llgoStdioNobuf, "")
ctx := &context{
prog: llssa.NewProgram(nil),
buildConf: &Config{
BuildMode: BuildModeExe,
Goos: "linux",
Goarch: "amd64",
},
}
pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"}
mod := genMainModule(ctx, llssa.PkgRuntime, pkg, true, true)
if mod.ExportFile != "foo.a-main" {
t.Fatalf("unexpected export file: %s", mod.ExportFile)
}
ir := mod.LPkg.String()
checks := []string{
"define i32 @main(",
"call void @Py_Initialize()",
"call void @\"example.com/foo.init\"()",
"define weak void @_start()",
}
for _, want := range checks {
if !strings.Contains(ir, want) {
t.Fatalf("main module IR missing %q:\n%s", want, ir)
}
}
}
func TestGenMainModuleLibrary(t *testing.T) {
llvm.InitializeAllTargets()
t.Setenv(llgoStdioNobuf, "")
ctx := &context{
prog: llssa.NewProgram(nil),
buildConf: &Config{
BuildMode: BuildModeCArchive,
Goos: "linux",
Goarch: "amd64",
},
}
pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"}
mod := genMainModule(ctx, llssa.PkgRuntime, pkg, false, false)
ir := mod.LPkg.String()
if strings.Contains(ir, "define i32 @main") {
t.Fatalf("library mode should not emit main function:\n%s", ir)
}
if !strings.Contains(ir, "@__llgo_argc = global i32 0") {
t.Fatalf("library mode missing argc global:\n%s", ir)
}
}

109
internal/build/resolver.go Normal file
View File

@@ -0,0 +1,109 @@
package build
import "strings"
// nameResolver maps symbol names to aggregation buckets based on the requested level.
type nameResolver struct {
level string
pkgs []Package
moduleMap map[string]string
packageMap map[string]string
}
func newNameResolver(level string, pkgs []Package) *nameResolver {
lvl := strings.ToLower(strings.TrimSpace(level))
if lvl == "" {
lvl = "module"
}
return &nameResolver{
level: lvl,
pkgs: pkgs,
moduleMap: make(map[string]string),
packageMap: make(map[string]string),
}
}
func (r *nameResolver) resolve(sym string) string {
base := moduleNameFromSymbol(sym)
symbol := trimSymbolForMatch(sym)
switch r.level {
case "full":
return base
case "package":
if pkg := r.matchPackage(symbol); pkg != "" {
return pkg
}
case "module":
if mod := r.matchModule(symbol); mod != "" {
return mod
}
}
if strings.Contains(symbol, "llgo") {
return "llgo-stubs"
}
return base
}
func (r *nameResolver) matchPackage(symbol string) string {
if symbol == "" {
return ""
}
if cached := r.packageMap[symbol]; cached != "" {
return cached
}
for _, pkg := range r.pkgs {
if pkg == nil || pkg.Package == nil {
continue
}
id := pkg.PkgPath
if id == "" {
continue
}
if strings.HasPrefix(symbol, id+".") {
r.packageMap[symbol] = id
return id
}
}
return ""
}
func (r *nameResolver) matchModule(symbol string) string {
if symbol == "" {
return ""
}
if cached := r.moduleMap[symbol]; cached != "" {
return cached
}
for _, pkg := range r.pkgs {
if pkg == nil || pkg.Package == nil {
continue
}
path := pkg.PkgPath
if path == "" {
continue
}
if strings.HasPrefix(symbol, path+".") {
mod := path
if pkg.Module != nil && pkg.Module.Path != "" {
mod = pkg.Module.Path
}
r.moduleMap[symbol] = mod
return mod
}
}
return ""
}
func trimSymbolForMatch(sym string) string {
name := strings.TrimSpace(sym)
for len(name) > 0 && (name[0] == '_' || name[0] == '.') {
name = name[1:]
}
if idx := strings.Index(name, " "); idx >= 0 {
name = name[:idx]
}
if idx := strings.Index(name, "@"); idx >= 0 {
name = name[:idx]
}
return name
}

View File

@@ -0,0 +1,627 @@
package build
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"os"
"path/filepath"
"sort"
"strconv"
"strings"
"github.com/goplus/llgo/xtool/env/llvm"
)
type sectionKind int
const (
sectionUnknown sectionKind = iota
sectionText
sectionROData
sectionData
sectionBSS
)
const (
// readelfInitialBuffer is the initial buffer size for reading readelf output.
// Most lines in readelf output are less than 1KB.
readelfInitialBuffer = 64 * 1024
// readelfMaxBuffer is the maximum buffer size to handle very long symbol names
// or section dumps. Reduced from 64MB to prevent excessive memory consumption
// while still accommodating reasonably large binaries.
readelfMaxBuffer = 4 * 1024 * 1024
)
// ELF special section indices (from ELF specification)
const (
SHN_UNDEF = 0x0000 // Undefined section
SHN_LORESERVE = 0xFF00 // Start of reserved indices
SHN_ABS = 0xFFF1 // Absolute values
SHN_COMMON = 0xFFF2 // Common symbols
SHN_XINDEX = 0xFFFF // Escape value for extended section indices
)
type sectionInfo struct {
Index int
Name string
Segment string
Address uint64
Size uint64
Kind sectionKind
}
type symbolInfo struct {
Name string
SectionIndex int
Address uint64
}
type readelfData struct {
sections map[int]*sectionInfo
symbols map[int][]symbolInfo
}
type moduleSize struct {
Name string
Code uint64
ROData uint64
Data uint64
BSS uint64
}
func (m *moduleSize) Flash() uint64 {
return m.Code + m.ROData + m.Data
}
func (m *moduleSize) RAM() uint64 {
return m.Data + m.BSS
}
type sizeReport struct {
Binary string
Modules map[string]*moduleSize
Total moduleSize
}
func (r *sizeReport) module(name string) *moduleSize {
if name == "" {
name = "(anonymous)"
}
if r.Modules == nil {
r.Modules = make(map[string]*moduleSize)
}
m, ok := r.Modules[name]
if !ok {
m = &moduleSize{Name: name}
r.Modules[name] = m
}
return m
}
func (r *sizeReport) add(name string, kind sectionKind, size uint64) {
if size == 0 {
return
}
m := r.module(name)
switch kind {
case sectionText:
m.Code += size
r.Total.Code += size
case sectionROData:
m.ROData += size
r.Total.ROData += size
case sectionData:
m.Data += size
r.Total.Data += size
case sectionBSS:
m.BSS += size
r.Total.BSS += size
}
}
func reportBinarySize(path, format, level string, pkgs []Package) error {
report, err := collectBinarySize(path, pkgs, level)
if err != nil {
return err
}
switch format {
case "", "text":
printTextReport(os.Stdout, report)
case "json":
return emitJSONReport(os.Stdout, report)
default:
return fmt.Errorf("unknown size format %q (valid: text,json)", format)
}
return nil
}
func collectBinarySize(path string, pkgs []Package, level string) (*sizeReport, error) {
cmd, err := llvm.New("").Readelf("--elf-output-style=LLVM", "--all", path)
if err != nil {
return nil, fmt.Errorf("llvm-readelf: %w", err)
}
var stderr bytes.Buffer
cmd.Stderr = &stderr
stdout, err := cmd.StdoutPipe()
if err != nil {
return nil, fmt.Errorf("llvm-readelf stdout: %w", err)
}
if err := cmd.Start(); err != nil {
return nil, fmt.Errorf("failed to execute llvm-readelf: %w", err)
}
parsed, parseErr := parseReadelfOutput(stdout)
closeErr := stdout.Close()
waitErr := cmd.Wait()
if waitErr != nil {
return nil, fmt.Errorf("llvm-readelf failed: %w\n%s", waitErr, stderr.String())
}
if parseErr != nil {
return nil, fmt.Errorf("parsing llvm-readelf output failed: %w", parseErr)
}
if closeErr != nil {
return nil, fmt.Errorf("closing llvm-readelf stdout pipe failed: %w", closeErr)
}
report := buildSizeReport(path, parsed, pkgs, level)
if report == nil || len(report.Modules) == 0 {
return nil, fmt.Errorf("size report: no allocatable sections found in %s", path)
}
return report, nil
}
func parseReadelfOutput(r io.Reader) (*readelfData, error) {
scanner := bufio.NewScanner(r)
scanner.Buffer(make([]byte, 0, readelfInitialBuffer), readelfMaxBuffer)
type ctxKind int
const (
ctxRoot ctxKind = iota
ctxSections
ctxSection
ctxSymbols
ctxSymbol
)
type ctx struct {
kind ctxKind
indent int
}
stack := []ctx{{kind: ctxRoot, indent: -1}}
push := func(kind ctxKind, indent int) {
stack = append(stack, ctx{kind: kind, indent: indent})
}
pop := func(expected ctxKind, indent int) bool {
top := stack[len(stack)-1]
if top.kind != expected || top.indent != indent {
return false
}
stack = stack[:len(stack)-1]
return true
}
current := func() ctx {
return stack[len(stack)-1]
}
data := &readelfData{
sections: make(map[int]*sectionInfo),
symbols: make(map[int][]symbolInfo),
}
// readelf outputs section references differently:
// - Mach-O: section numbers are 1-based in symbol references
// - ELF: section numbers in symbol references match the Index directly
secIndexBase := 1 // default to Mach-O behavior; switch to 0 for ELF once detected
var currentSection *sectionInfo
var currentSymbol *symbolInfo
for scanner.Scan() {
raw := scanner.Text()
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
continue
}
// Detect object format early to adjust section index base
if strings.HasPrefix(trimmed, "Format:") {
lower := strings.ToLower(trimmed)
if strings.Contains(lower, "mach-o") {
secIndexBase = 1
} else if strings.Contains(lower, "elf") {
secIndexBase = 0
}
}
indent := countLeadingSpaces(raw)
top := current()
switch {
case strings.HasPrefix(trimmed, "Sections [") && top.kind == ctxRoot:
push(ctxSections, indent)
continue
case strings.HasPrefix(trimmed, "Symbols [") && top.kind == ctxRoot:
push(ctxSymbols, indent)
continue
case trimmed == "Section {" && top.kind == ctxSections && indent == top.indent+2:
currentSection = &sectionInfo{Index: -1}
push(ctxSection, indent)
continue
case trimmed == "Symbol {" && top.kind == ctxSymbols && indent == top.indent+2:
currentSymbol = &symbolInfo{SectionIndex: -1}
push(ctxSymbol, indent)
continue
case trimmed == "}" && pop(ctxSection, indent):
if currentSection != nil && currentSection.Index >= 0 {
currentSection.Kind = classifySection(currentSection.Name, currentSection.Segment)
data.sections[currentSection.Index] = currentSection
}
currentSection = nil
continue
case trimmed == "}" && pop(ctxSymbol, indent):
if currentSymbol != nil && currentSymbol.SectionIndex >= 0 {
data.symbols[currentSymbol.SectionIndex] = append(data.symbols[currentSymbol.SectionIndex], *currentSymbol)
}
currentSymbol = nil
continue
case trimmed == "]" && (top.kind == ctxSections || top.kind == ctxSymbols) && indent == top.indent:
stack = stack[:len(stack)-1]
continue
}
switch top.kind {
case ctxSection:
if currentSection == nil {
continue
}
switch {
case strings.HasPrefix(trimmed, "Index: "):
if idx, err := strconv.Atoi(strings.TrimSpace(trimmed[len("Index: "):])); err == nil {
currentSection.Index = idx
}
case strings.HasPrefix(trimmed, "Name: "):
currentSection.Name = parseNameField(trimmed[len("Name: "):])
case strings.HasPrefix(trimmed, "Segment: "):
currentSection.Segment = parseNameField(trimmed[len("Segment: "):])
case strings.HasPrefix(trimmed, "Address: "):
if val, err := parseUintField(trimmed[len("Address: "):]); err == nil {
currentSection.Address = val
}
case strings.HasPrefix(trimmed, "Size: "):
if val, err := parseUintField(trimmed[len("Size: "):]); err == nil {
currentSection.Size = val
}
}
case ctxSymbol:
if currentSymbol == nil {
continue
}
switch {
case strings.HasPrefix(trimmed, "Name: "):
currentSymbol.Name = parseNameField(trimmed[len("Name: "):])
case strings.HasPrefix(trimmed, "Section: "):
name, idx := parseSectionRef(trimmed[len("Section: "):], secIndexBase)
currentSymbol.SectionIndex = idx
if currentSymbol.Name == "" {
currentSymbol.Name = name
}
case strings.HasPrefix(trimmed, "Value: "):
if val, err := parseUintField(trimmed[len("Value: "):]); err == nil {
currentSymbol.Address = val
}
}
}
}
if err := scanner.Err(); err != nil {
return nil, err
}
return data, nil
}
func countLeadingSpaces(line string) int {
count := 0
for _, ch := range line {
if ch != ' ' {
break
}
count++
}
return count
}
func classifySection(name, segment string) sectionKind {
ln := strings.ToLower(name)
ls := strings.ToLower(segment)
switch {
case strings.Contains(ln, "text"), strings.Contains(ln, "code"), strings.Contains(ln, "plt"):
return sectionText
case strings.Contains(ln, "rodata"), strings.Contains(ln, "const"), strings.Contains(ln, "literal"), strings.Contains(ln, "cstring"):
return sectionROData
case strings.Contains(ln, "bss"), strings.Contains(ln, "tbss"), strings.Contains(ln, "sbss"), strings.Contains(ln, "common"), strings.Contains(ln, "zerofill"):
return sectionBSS
case strings.Contains(ln, "data"), strings.Contains(ln, "got"), strings.Contains(ln, "init_array"), strings.Contains(ln, "cfstring"), strings.Contains(ln, "tdata"):
return sectionData
}
switch {
case strings.Contains(ls, "__text"):
return sectionText
case strings.Contains(ls, "data_const"):
return sectionROData
case strings.Contains(ls, "__data"):
return sectionData
}
return sectionUnknown
}
func buildSizeReport(path string, data *readelfData, pkgs []Package, level string) *sizeReport {
report := &sizeReport{Binary: path, Modules: make(map[string]*moduleSize)}
if data == nil {
return report
}
res := newNameResolver(level, pkgs)
var recognized bool
for idx, sec := range data.sections {
if sec == nil || sec.Size == 0 {
continue
}
if sec.Kind == sectionUnknown {
continue
}
recognized = true
end := sec.Address + sec.Size
syms := data.symbols[idx]
if len(syms) == 0 {
report.add("(unknown "+sec.Name+")", sec.Kind, sec.Size)
continue
}
// Sort symbols by address to calculate sizes based on address ranges
sort.Slice(syms, func(i, j int) bool {
if syms[i].Address == syms[j].Address {
return syms[i].Name < syms[j].Name
}
return syms[i].Address < syms[j].Address
})
cursor := sec.Address
for i := 0; i < len(syms); i++ {
sym := syms[i]
// Skip symbols that are beyond the section bounds
if sym.Address >= end {
continue
}
addr := sym.Address
// Clamp symbol address to section start if it's before the section
if addr < sec.Address {
addr = sec.Address
}
// Add padding bytes between cursor and current symbol
if addr > cursor {
report.add("(padding "+sec.Name+")", sec.Kind, addr-cursor)
cursor = addr
}
// Find the next symbol address to calculate this symbol's size.
// Symbols at the same address are handled by taking the next different address.
next := end
// Optimize: check next symbol first before scanning
if i+1 < len(syms) && syms[i+1].Address > addr {
next = syms[i+1].Address
} else {
// Only search if next symbol is at same address
for j := i + 1; j < len(syms); j++ {
if syms[j].Address > addr {
next = syms[j].Address
break
}
}
}
if next > end {
next = end
}
// Skip symbols with zero size
if next <= addr {
continue
}
// Attribute the address range [addr, next) to the symbol's module
mod := res.resolve(sym.Name)
report.add(mod, sec.Kind, next-addr)
cursor = next
}
// Add any remaining padding at the end of the section
if cursor < end {
report.add("(padding "+sec.Name+")", sec.Kind, end-cursor)
}
}
if !recognized {
return nil
}
return report
}
func emitJSONReport(w io.Writer, report *sizeReport) error {
type moduleJSON struct {
Name string `json:"name"`
Code uint64 `json:"code"`
ROData uint64 `json:"rodata"`
Data uint64 `json:"data"`
BSS uint64 `json:"bss"`
Flash uint64 `json:"flash"`
RAM uint64 `json:"ram"`
}
mods := report.sortedModules()
jsonMods := make([]moduleJSON, 0, len(mods))
for _, m := range mods {
jsonMods = append(jsonMods, moduleJSON{
Name: m.Name,
Code: m.Code,
ROData: m.ROData,
Data: m.Data,
BSS: m.BSS,
Flash: m.Flash(),
RAM: m.RAM(),
})
}
payload := struct {
Binary string `json:"binary"`
Modules []moduleJSON `json:"modules"`
Total moduleJSON `json:"total"`
}{
Binary: filepath.Clean(report.Binary),
Modules: jsonMods,
Total: moduleJSON{
Name: "total",
Code: report.Total.Code,
ROData: report.Total.ROData,
Data: report.Total.Data,
BSS: report.Total.BSS,
Flash: report.Total.Flash(),
RAM: report.Total.RAM(),
},
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(payload)
}
func printTextReport(w io.Writer, report *sizeReport) {
fmt.Fprintf(w, "\nSize report for %s\n", filepath.Clean(report.Binary))
fmt.Fprintln(w, " code rodata data bss | flash ram | module")
fmt.Fprintln(w, "------------------------------- | --------------- | ----------------")
for _, m := range report.sortedModules() {
fmt.Fprintf(w, "%7d %7d %7d %7d | %7d %7d | %s\n", m.Code, m.ROData, m.Data, m.BSS, m.Flash(), m.RAM(), m.Name)
}
fmt.Fprintln(w, "------------------------------- | --------------- | ----------------")
fmt.Fprintf(w, "%7d %7d %7d %7d | %7d %7d | total\n", report.Total.Code, report.Total.ROData, report.Total.Data, report.Total.BSS, report.Total.Flash(), report.Total.RAM())
}
func (r *sizeReport) sortedModules() []*moduleSize {
mods := make([]*moduleSize, 0, len(r.Modules))
for _, m := range r.Modules {
mods = append(mods, m)
}
sort.Slice(mods, func(i, j int) bool {
if mods[i].Flash() == mods[j].Flash() {
return mods[i].Name < mods[j].Name
}
return mods[i].Flash() > mods[j].Flash()
})
return mods
}
// moduleNameFromSymbol extracts the Go package name from a symbol name.
// It handles various symbol naming conventions:
// - C symbols: Strip leading underscore (e.g., "_main" -> "main")
// - Assembler symbols: Strip leading dot (e.g., ".text" -> "text")
// - Versioned symbols: Remove version suffix (e.g., "symbol@@GLIBC_2.2.5" -> "symbol")
// - Go symbols: Extract package from "package.symbol" format
// - Generic types: Strip type parameters (e.g., "pkg(T)" -> "pkg")
func moduleNameFromSymbol(raw string) string {
name := strings.TrimSpace(raw)
// Strip C symbol prefix
name = strings.TrimPrefix(name, "_")
// Strip assembler symbol prefix
name = strings.TrimPrefix(name, ".")
if name == "" {
return "(anonymous)"
}
// Remove trailing attributes (e.g., "symbol (weak)")
if idx := strings.Index(name, " "); idx > 0 {
name = name[:idx]
}
// Remove version suffix for versioned symbols (e.g., "symbol@@GLIBC_2.2.5")
if idx := strings.Index(name, "@"); idx > 0 {
name = name[:idx]
}
// Extract Go package name from "package.symbol" format
lastDot := strings.LastIndex(name, ".")
if lastDot > 0 {
pkg := name[:lastDot]
// Strip generic type parameters (e.g., "slices.Sort[int]" -> "slices")
if paren := strings.Index(pkg, "("); paren > 0 {
pkg = pkg[:paren]
}
pkg = strings.Trim(pkg, " ")
if pkg != "" {
return pkg
}
}
return name
}
func parseNameField(field string) string {
val := strings.TrimSpace(field)
if idx := strings.Index(val, "("); idx >= 0 {
val = strings.TrimSpace(val[:idx])
}
return val
}
func parseSectionRef(field string, indexBase int) (string, int) {
name := parseNameField(field)
idx := strings.Index(field, "(")
if idx < 0 {
return name, -1
}
end := strings.Index(field[idx:], ")")
if end < 0 {
return name, -1
}
val := strings.TrimSpace(field[idx+1 : idx+end])
val = strings.TrimPrefix(val, "0x")
if val == "" {
return name, -1
}
num, err := strconv.ParseUint(val, 16, 64)
if err != nil {
return name, -1
}
if num == 0 {
return name, -1
}
if indexBase == 0 && num >= SHN_LORESERVE {
// Special ELF section indices (SHN_ABS, SHN_COMMON, etc.)
return name, -1
}
if num > math.MaxInt {
return name, -1
}
res := int(num) - indexBase
if res < 0 {
return name, -1
}
return name, res
}
func parseUintField(field string) (uint64, error) {
val := strings.TrimSpace(field)
if strings.HasPrefix(val, "0x") || strings.HasPrefix(val, "0X") {
return strconv.ParseUint(val[2:], 16, 64)
}
return strconv.ParseUint(val, 10, 64)
}
func ensureSizeReporting(conf *Config) error {
if !conf.SizeReport {
return nil
}
switch strings.ToLower(conf.SizeLevel) {
case "", "module":
conf.SizeLevel = "module"
case "package", "full":
conf.SizeLevel = strings.ToLower(conf.SizeLevel)
default:
return fmt.Errorf("invalid size level %q (valid: full,module,package)", conf.SizeLevel)
}
cmd, err := llvm.New("").Readelf("--version")
if err != nil {
return fmt.Errorf("llvm-readelf not available: %w", err)
}
cmd.Stdout = io.Discard
cmd.Stderr = io.Discard
if err := cmd.Run(); err != nil {
return fmt.Errorf("llvm-readelf not available: %w", err)
}
return nil
}

View File

@@ -0,0 +1,156 @@
//go:build !llgo
package build
import (
"bytes"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"golang.org/x/tools/go/packages"
)
const sampleReadelf = `Sections [
Section {
Index: 0
Name: __text (5F)
Segment: __TEXT (5F)
Address: 0x1000
Size: 0x20
}
Section {
Index: 1
Name: __data
Segment: __DATA
Address: 0x2000
Size: 0x10
}
Section {
Index: 2
Name: __common
Segment: __DATA
Address: 0x3000
Size: 0x8
}
]
Symbols [
Symbol {
Name: _main.main
Section: __text (0x1)
Value: 0x1000
}
Symbol {
Name: _runtime.init
Section: __text (0x1)
Value: 0x1010
}
Symbol {
Name: _main.dataVar
Section: __data (0x2)
Value: 0x2000
}
Symbol {
Name: _runtime.dataVar
Section: __data (0x2)
Value: 0x2008
}
Symbol {
Name: _runtime.bssVar
Section: __common (0x3)
Value: 0x3000
}
]
`
func TestParseReadelfOutput(t *testing.T) {
parsed, err := parseReadelfOutput(strings.NewReader(sampleReadelf))
if err != nil {
t.Fatalf("parseReadelfOutput: %v", err)
}
report := buildSizeReport("fake.bin", parsed, nil, "")
if report == nil {
t.Fatal("expected report")
}
modules := report.Modules
if len(modules) == 0 {
t.Fatal("expected modules in report")
}
mainMod, ok := modules["main"]
if !ok {
t.Fatalf("expected main module, got %v", modules)
}
if mainMod.Code != 0x10 {
t.Fatalf("unexpected main code size: %d", mainMod.Code)
}
if mainMod.Data != 0x8 {
t.Fatalf("unexpected main data size: %d", mainMod.Data)
}
runtimeMod := modules["runtime"]
if runtimeMod.Code != 0x10 {
t.Fatalf("unexpected runtime code size: %d", runtimeMod.Code)
}
if runtimeMod.Data != 0x8 {
t.Fatalf("unexpected runtime data size: %d", runtimeMod.Data)
}
if runtimeMod.BSS != 0x8 {
t.Fatalf("unexpected runtime bss size: %d", runtimeMod.BSS)
}
if report.Total.Flash() != 0x10+0x10+0x8+0x8 {
t.Fatalf("unexpected flash total: %d", report.Total.Flash())
}
if report.Total.RAM() != 0x8+0x8+0x8 {
t.Fatalf("unexpected ram total: %d", report.Total.RAM())
}
}
func TestParseReadelfRealBinary(t *testing.T) {
path := os.Getenv("LLGO_SIZE_REPORT_BIN")
if path == "" {
return
}
absPath, err := filepath.Abs(path)
if err != nil {
t.Fatalf("abs path: %v", err)
}
cmd := exec.Command("llvm-readelf", "--all", absPath)
var buf bytes.Buffer
cmd.Stdout = &buf
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
t.Fatalf("llvm-readelf failed: %v", err)
}
parsed, err := parseReadelfOutput(bytes.NewReader(buf.Bytes()))
if err != nil {
t.Fatalf("parseReadelfOutput(real): %v", err)
}
if len(parsed.sections) == 0 {
t.Fatal("expected sections in real binary")
}
report := buildSizeReport(absPath, parsed, nil, "")
if len(report.Modules) == 0 {
t.Fatalf("expected modules for %s", path)
}
}
func TestNameResolver(t *testing.T) {
pkgs := []Package{
&aPackage{Package: &packages.Package{PkgPath: "github.com/foo/bar", Module: &packages.Module{Path: "github.com/foo"}}},
}
symbol := "_github.com/foo/bar.Type.method"
if got := newNameResolver("package", pkgs).resolve(symbol); got != "github.com/foo/bar" {
t.Fatalf("package level want github.com/foo/bar, got %q", got)
}
if got := newNameResolver("module", pkgs).resolve(symbol); got != "github.com/foo" {
t.Fatalf("module level want github.com/foo, got %q", got)
}
full := newNameResolver("full", pkgs).resolve(symbol)
if full != "github.com/foo/bar.Type" {
t.Fatalf("full level unexpected: %q", full)
}
if got := newNameResolver("package", nil).resolve("_llgo_stub.foo"); got != "llgo-stubs" {
t.Fatalf("llgo default grouping failed: %q", got)
}
}

View File

@@ -1,3 +1,5 @@
//go:build !llgo
package libc
import (

View File

@@ -30,8 +30,6 @@ const (
var buildVersion string
// Version returns the version of the running LLGo binary.
//
//export LLGoVersion
func Version() string {
if buildVersion != "" {
return buildVersion

View File

@@ -1,622 +0,0 @@
// 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 parser
import (
"fmt"
"go/ast"
"go/token"
"strings"
)
const debugResolve = false
// resolveFile walks the given file to resolve identifiers within the file
// scope, updating ast.Ident.Obj fields with declaration information.
//
// If declErr is non-nil, it is used to report declaration errors during
// resolution. tok is used to format position in error messages.
func resolveFile(file *ast.File, handle *token.File, declErr func(token.Pos, string)) {
pkgScope := ast.NewScope(nil)
r := &resolver{
handle: handle,
declErr: declErr,
topScope: pkgScope,
pkgScope: pkgScope,
depth: 1,
}
for _, decl := range file.Decls {
ast.Walk(r, decl)
}
r.closeScope()
assert(r.topScope == nil, "unbalanced scopes")
assert(r.labelScope == nil, "unbalanced label scopes")
// resolve global identifiers within the same file
i := 0
for _, ident := range r.unresolved {
// i <= index for current ident
assert(ident.Obj == unresolved, "object already resolved")
ident.Obj = r.pkgScope.Lookup(ident.Name) // also removes unresolved sentinel
if ident.Obj == nil {
r.unresolved[i] = ident
i++
} else if debugResolve {
pos := ident.Obj.Decl.(interface{ Pos() token.Pos }).Pos()
r.trace("resolved %s@%v to package object %v", ident.Name, ident.Pos(), pos)
}
}
file.Scope = r.pkgScope
file.Unresolved = r.unresolved[0:i]
}
const maxScopeDepth int = 1e3
type resolver struct {
handle *token.File
declErr func(token.Pos, string)
// Ordinary identifier scopes
pkgScope *ast.Scope // pkgScope.Outer == nil
topScope *ast.Scope // top-most scope; may be pkgScope
unresolved []*ast.Ident // unresolved identifiers
depth int // scope depth
// Label scopes
// (maintained by open/close LabelScope)
labelScope *ast.Scope // label scope for current function
targetStack [][]*ast.Ident // stack of unresolved labels
}
func (r *resolver) trace(format string, args ...any) {
fmt.Println(strings.Repeat(". ", r.depth) + r.sprintf(format, args...))
}
func (r *resolver) sprintf(format string, args ...any) string {
for i, arg := range args {
switch arg := arg.(type) {
case token.Pos:
args[i] = r.handle.Position(arg)
}
}
return fmt.Sprintf(format, args...)
}
func (r *resolver) openScope(pos token.Pos) {
r.depth++
if r.depth > maxScopeDepth {
panic(bailout{pos: pos, msg: "exceeded max scope depth during object resolution"})
}
if debugResolve {
r.trace("opening scope @%v", pos)
}
r.topScope = ast.NewScope(r.topScope)
}
func (r *resolver) closeScope() {
r.depth--
if debugResolve {
r.trace("closing scope")
}
r.topScope = r.topScope.Outer
}
func (r *resolver) openLabelScope() {
r.labelScope = ast.NewScope(r.labelScope)
r.targetStack = append(r.targetStack, nil)
}
func (r *resolver) closeLabelScope() {
// resolve labels
n := len(r.targetStack) - 1
scope := r.labelScope
for _, ident := range r.targetStack[n] {
ident.Obj = scope.Lookup(ident.Name)
if ident.Obj == nil && r.declErr != nil {
r.declErr(ident.Pos(), fmt.Sprintf("label %s undefined", ident.Name))
}
}
// pop label scope
r.targetStack = r.targetStack[0:n]
r.labelScope = r.labelScope.Outer
}
func (r *resolver) declare(decl, data any, scope *ast.Scope, kind ast.ObjKind, idents ...*ast.Ident) {
for _, ident := range idents {
if ident.Obj != nil {
panic(fmt.Sprintf("%v: identifier %s already declared or resolved", ident.Pos(), ident.Name))
}
obj := ast.NewObj(kind, ident.Name)
// remember the corresponding declaration for redeclaration
// errors and global variable resolution/typechecking phase
obj.Decl = decl
obj.Data = data
// Identifiers (for receiver type parameters) are written to the scope, but
// never set as the resolved object. See go.dev/issue/50956.
if _, ok := decl.(*ast.Ident); !ok {
ident.Obj = obj
}
if ident.Name != "_" {
if debugResolve {
r.trace("declaring %s@%v", ident.Name, ident.Pos())
}
if alt := scope.Insert(obj); alt != nil && r.declErr != nil {
prevDecl := ""
if pos := alt.Pos(); pos.IsValid() {
prevDecl = r.sprintf("\n\tprevious declaration at %v", pos)
}
r.declErr(ident.Pos(), fmt.Sprintf("%s redeclared in this block%s", ident.Name, prevDecl))
}
}
}
}
func (r *resolver) shortVarDecl(decl *ast.AssignStmt) {
// Go spec: A short variable declaration may redeclare variables
// provided they were originally declared in the same block with
// the same type, and at least one of the non-blank variables is new.
n := 0 // number of new variables
for _, x := range decl.Lhs {
if ident, isIdent := x.(*ast.Ident); isIdent {
assert(ident.Obj == nil, "identifier already declared or resolved")
obj := ast.NewObj(ast.Var, ident.Name)
// remember corresponding assignment for other tools
obj.Decl = decl
ident.Obj = obj
if ident.Name != "_" {
if debugResolve {
r.trace("declaring %s@%v", ident.Name, ident.Pos())
}
if alt := r.topScope.Insert(obj); alt != nil {
ident.Obj = alt // redeclaration
} else {
n++ // new declaration
}
}
}
}
if n == 0 && r.declErr != nil {
r.declErr(decl.Lhs[0].Pos(), "no new variables on left side of :=")
}
}
// The unresolved object is a sentinel to mark identifiers that have been added
// to the list of unresolved identifiers. The sentinel is only used for verifying
// internal consistency.
var unresolved = new(ast.Object)
// If x is an identifier, resolve attempts to resolve x by looking up
// the object it denotes. If no object is found and collectUnresolved is
// set, x is marked as unresolved and collected in the list of unresolved
// identifiers.
func (r *resolver) resolve(ident *ast.Ident, collectUnresolved bool) {
if ident.Obj != nil {
panic(r.sprintf("%v: identifier %s already declared or resolved", ident.Pos(), ident.Name))
}
// '_' should never refer to existing declarations, because it has special
// handling in the spec.
if ident.Name == "_" {
return
}
for s := r.topScope; s != nil; s = s.Outer {
if obj := s.Lookup(ident.Name); obj != nil {
if debugResolve {
r.trace("resolved %v:%s to %v", ident.Pos(), ident.Name, obj)
}
assert(obj.Name != "", "obj with no name")
// Identifiers (for receiver type parameters) are written to the scope,
// but never set as the resolved object. See go.dev/issue/50956.
if _, ok := obj.Decl.(*ast.Ident); !ok {
ident.Obj = obj
}
return
}
}
// all local scopes are known, so any unresolved identifier
// must be found either in the file scope, package scope
// (perhaps in another file), or universe scope --- collect
// them so that they can be resolved later
if collectUnresolved {
ident.Obj = unresolved
r.unresolved = append(r.unresolved, ident)
}
}
func (r *resolver) walkExprs(list []ast.Expr) {
for _, node := range list {
ast.Walk(r, node)
}
}
func Unparen(e ast.Expr) ast.Expr {
for {
paren, ok := e.(*ast.ParenExpr)
if !ok {
return e
}
e = paren.X
}
}
func (r *resolver) walkLHS(list []ast.Expr) {
for _, expr := range list {
expr := Unparen(expr)
if _, ok := expr.(*ast.Ident); !ok && expr != nil {
ast.Walk(r, expr)
}
}
}
func (r *resolver) walkStmts(list []ast.Stmt) {
for _, stmt := range list {
ast.Walk(r, stmt)
}
}
func (r *resolver) Visit(node ast.Node) ast.Visitor {
if debugResolve && node != nil {
r.trace("node %T@%v", node, node.Pos())
}
switch n := node.(type) {
// Expressions.
case *ast.Ident:
r.resolve(n, true)
case *ast.FuncLit:
r.openScope(n.Pos())
defer r.closeScope()
r.walkFuncType(n.Type)
r.walkBody(n.Body)
case *ast.SelectorExpr:
ast.Walk(r, n.X)
// Note: don't try to resolve n.Sel, as we don't support qualified
// resolution.
case *ast.StructType:
r.openScope(n.Pos())
defer r.closeScope()
r.walkFieldList(n.Fields, ast.Var)
case *ast.FuncType:
r.openScope(n.Pos())
defer r.closeScope()
r.walkFuncType(n)
case *ast.CompositeLit:
if n.Type != nil {
ast.Walk(r, n.Type)
}
for _, e := range n.Elts {
if kv, _ := e.(*ast.KeyValueExpr); kv != nil {
// See go.dev/issue/45160: try to resolve composite lit keys, but don't
// collect them as unresolved if resolution failed. This replicates
// existing behavior when resolving during parsing.
if ident, _ := kv.Key.(*ast.Ident); ident != nil {
r.resolve(ident, false)
} else {
ast.Walk(r, kv.Key)
}
ast.Walk(r, kv.Value)
} else {
ast.Walk(r, e)
}
}
case *ast.InterfaceType:
r.openScope(n.Pos())
defer r.closeScope()
r.walkFieldList(n.Methods, ast.Fun)
// Statements
case *ast.LabeledStmt:
r.declare(n, nil, r.labelScope, ast.Lbl, n.Label)
ast.Walk(r, n.Stmt)
case *ast.AssignStmt:
r.walkExprs(n.Rhs)
if n.Tok == token.DEFINE {
r.shortVarDecl(n)
} else {
r.walkExprs(n.Lhs)
}
case *ast.BranchStmt:
// add to list of unresolved targets
if n.Tok != token.FALLTHROUGH && n.Label != nil {
depth := len(r.targetStack) - 1
r.targetStack[depth] = append(r.targetStack[depth], n.Label)
}
case *ast.BlockStmt:
r.openScope(n.Pos())
defer r.closeScope()
r.walkStmts(n.List)
case *ast.IfStmt:
r.openScope(n.Pos())
defer r.closeScope()
if n.Init != nil {
ast.Walk(r, n.Init)
}
ast.Walk(r, n.Cond)
ast.Walk(r, n.Body)
if n.Else != nil {
ast.Walk(r, n.Else)
}
case *ast.CaseClause:
r.walkExprs(n.List)
r.openScope(n.Pos())
defer r.closeScope()
r.walkStmts(n.Body)
case *ast.SwitchStmt:
r.openScope(n.Pos())
defer r.closeScope()
if n.Init != nil {
ast.Walk(r, n.Init)
}
if n.Tag != nil {
// The scope below reproduces some unnecessary behavior of the parser,
// opening an extra scope in case this is a type switch. It's not needed
// for expression switches.
// TODO: remove this once we've matched the parser resolution exactly.
if n.Init != nil {
r.openScope(n.Tag.Pos())
defer r.closeScope()
}
ast.Walk(r, n.Tag)
}
if n.Body != nil {
r.walkStmts(n.Body.List)
}
case *ast.TypeSwitchStmt:
if n.Init != nil {
r.openScope(n.Pos())
defer r.closeScope()
ast.Walk(r, n.Init)
}
r.openScope(n.Assign.Pos())
defer r.closeScope()
ast.Walk(r, n.Assign)
// s.Body consists only of case clauses, so does not get its own
// scope.
if n.Body != nil {
r.walkStmts(n.Body.List)
}
case *ast.CommClause:
r.openScope(n.Pos())
defer r.closeScope()
if n.Comm != nil {
ast.Walk(r, n.Comm)
}
r.walkStmts(n.Body)
case *ast.SelectStmt:
// as for switch statements, select statement bodies don't get their own
// scope.
if n.Body != nil {
r.walkStmts(n.Body.List)
}
case *ast.ForStmt:
r.openScope(n.Pos())
defer r.closeScope()
if n.Init != nil {
ast.Walk(r, n.Init)
}
if n.Cond != nil {
ast.Walk(r, n.Cond)
}
if n.Post != nil {
ast.Walk(r, n.Post)
}
ast.Walk(r, n.Body)
case *ast.RangeStmt:
r.openScope(n.Pos())
defer r.closeScope()
ast.Walk(r, n.X)
var lhs []ast.Expr
if n.Key != nil {
lhs = append(lhs, n.Key)
}
if n.Value != nil {
lhs = append(lhs, n.Value)
}
if len(lhs) > 0 {
if n.Tok == token.DEFINE {
// Note: we can't exactly match the behavior of object resolution
// during the parsing pass here, as it uses the position of the RANGE
// token for the RHS OpPos. That information is not contained within
// the AST.
as := &ast.AssignStmt{
Lhs: lhs,
Tok: token.DEFINE,
TokPos: n.TokPos,
Rhs: []ast.Expr{&ast.UnaryExpr{Op: token.RANGE, X: n.X}},
}
// TODO(rFindley): this walkLHS reproduced the parser resolution, but
// is it necessary? By comparison, for a normal AssignStmt we don't
// walk the LHS in case there is an invalid identifier list.
r.walkLHS(lhs)
r.shortVarDecl(as)
} else {
r.walkExprs(lhs)
}
}
ast.Walk(r, n.Body)
// Declarations
case *ast.GenDecl:
switch n.Tok {
case token.CONST, token.VAR:
for i, spec := range n.Specs {
spec := spec.(*ast.ValueSpec)
kind := ast.Con
if n.Tok == token.VAR {
kind = ast.Var
}
r.walkExprs(spec.Values)
if spec.Type != nil {
ast.Walk(r, spec.Type)
}
r.declare(spec, i, r.topScope, kind, spec.Names...)
}
case token.TYPE:
for _, spec := range n.Specs {
spec := spec.(*ast.TypeSpec)
// Go spec: The scope of a type identifier declared inside a function begins
// at the identifier in the TypeSpec and ends at the end of the innermost
// containing block.
r.declare(spec, nil, r.topScope, ast.Typ, spec.Name)
if spec.TypeParams != nil {
r.openScope(spec.Pos())
r.walkTParams(spec.TypeParams)
r.closeScope()
}
ast.Walk(r, spec.Type)
}
}
case *ast.FuncDecl:
// Open the function scope.
r.openScope(n.Pos())
defer r.closeScope()
r.walkRecv(n.Recv)
// Type parameters are walked normally: they can reference each other, and
// can be referenced by normal parameters.
if n.Type.TypeParams != nil {
r.walkTParams(n.Type.TypeParams)
// TODO(rFindley): need to address receiver type parameters.
}
// Resolve and declare parameters in a specific order to get duplicate
// declaration errors in the correct location.
r.resolveList(n.Type.Params)
r.resolveList(n.Type.Results)
r.declareList(n.Recv, ast.Var)
r.declareList(n.Type.Params, ast.Var)
r.declareList(n.Type.Results, ast.Var)
r.walkBody(n.Body)
if n.Recv == nil && n.Name.Name != "init" {
r.declare(n, nil, r.pkgScope, ast.Fun, n.Name)
}
default:
return r
}
return nil
}
func (r *resolver) walkFuncType(typ *ast.FuncType) {
// typ.TypeParams must be walked separately for FuncDecls.
r.resolveList(typ.Params)
r.resolveList(typ.Results)
r.declareList(typ.Params, ast.Var)
r.declareList(typ.Results, ast.Var)
}
func (r *resolver) resolveList(list *ast.FieldList) {
if list == nil {
return
}
for _, f := range list.List {
if f.Type != nil {
ast.Walk(r, f.Type)
}
}
}
func (r *resolver) declareList(list *ast.FieldList, kind ast.ObjKind) {
if list == nil {
return
}
for _, f := range list.List {
r.declare(f, nil, r.topScope, kind, f.Names...)
}
}
func (r *resolver) walkRecv(recv *ast.FieldList) {
// If our receiver has receiver type parameters, we must declare them before
// trying to resolve the rest of the receiver, and avoid re-resolving the
// type parameter identifiers.
if recv == nil || len(recv.List) == 0 {
return // nothing to do
}
typ := recv.List[0].Type
if ptr, ok := typ.(*ast.StarExpr); ok {
typ = ptr.X
}
var declareExprs []ast.Expr // exprs to declare
var resolveExprs []ast.Expr // exprs to resolve
switch typ := typ.(type) {
case *ast.IndexExpr:
declareExprs = []ast.Expr{typ.Index}
resolveExprs = append(resolveExprs, typ.X)
case *ast.IndexListExpr:
declareExprs = typ.Indices
resolveExprs = append(resolveExprs, typ.X)
default:
resolveExprs = append(resolveExprs, typ)
}
for _, expr := range declareExprs {
if id, _ := expr.(*ast.Ident); id != nil {
r.declare(expr, nil, r.topScope, ast.Typ, id)
} else {
// The receiver type parameter expression is invalid, but try to resolve
// it anyway for consistency.
resolveExprs = append(resolveExprs, expr)
}
}
for _, expr := range resolveExprs {
if expr != nil {
ast.Walk(r, expr)
}
}
// The receiver is invalid, but try to resolve it anyway for consistency.
for _, f := range recv.List[1:] {
if f.Type != nil {
ast.Walk(r, f.Type)
}
}
}
func (r *resolver) walkFieldList(list *ast.FieldList, kind ast.ObjKind) {
if list == nil {
return
}
r.resolveList(list)
r.declareList(list, kind)
}
// walkTParams is like walkFieldList, but declares type parameters eagerly so
// that they may be resolved in the constraint expressions held in the field
// Type.
func (r *resolver) walkTParams(list *ast.FieldList) {
r.declareList(list, ast.Typ)
r.resolveList(list)
}
func (r *resolver) walkBody(body *ast.BlockStmt) {
if body == nil {
return
}
r.openLabelScope()
defer r.closeLabelScope()
r.walkStmts(body.List)
}

View File

@@ -1 +0,0 @@
package testing

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -22,6 +22,7 @@ var hasAltPkg = map[string]none{
"crypto/sha256": {},
"crypto/sha512": {},
"crypto/subtle": {},
"go/build": {},
"go/parser": {},
"hash/crc32": {},
"hash/maphash": {},

View File

@@ -1,5 +1,4 @@
//go:build !nogc
// +build !nogc
//go:build !nogc && !baremetal
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,5 +1,4 @@
//go:build nogc
// +build nogc
//go:build nogc || baremetal
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,4 +1,4 @@
//go:build llgo
//go:build llgo && !baremetal
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,4 +1,4 @@
//go:build llgo && !nogc
//go:build llgo && !baremetal && !nogc
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -1,4 +1,4 @@
//go:build llgo && nogc
//go:build llgo && (nogc || baremetal)
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -0,0 +1,34 @@
//go:build !llgo || baremetal
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package tls
// Handle is a no-op TLS handle used when building without the llgo tag.
type Handle[T any] struct{}
// Alloc returns a stub TLS handle that ignores all operations.
func Alloc[T any](func(*T)) Handle[T] { return Handle[T]{} }
// Get always returns the zero value.
func (Handle[T]) Get() (zero T) { return zero }
// Set is a no-op.
func (Handle[T]) Set(T) {}
// Clear is a no-op.
func (Handle[T]) Clear() {}

View File

@@ -0,0 +1,19 @@
// Copyright 2024 The GoPlus Authors (goplus.org). All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package build provides alternative implementations for go/build.
// We override build.Default.Compiler in an init function.
package build
import (
"go/build"
)
func init() {
// LLGO PATCH: Override build.Default.Compiler to be "gc" instead of "llgo"
// This prevents "unknown compiler" errors when user code uses go/build package
// Even though runtime.Compiler = "llgo", we set build.Default.Compiler = "gc"
build.Default.Compiler = "gc"
}

View File

@@ -23,7 +23,7 @@ import (
"github.com/goplus/llgo/runtime/internal/runtime"
)
// llgo:skip init CompareString
// llgo:skip init
type _bytealg struct{}
func IndexByte(b []byte, ch byte) int {

View File

@@ -1759,6 +1759,16 @@ func ValueOf(i any) Value {
return unpackEface(i)
}
// Indirect returns the value that v points to.
// If v is a nil pointer, Indirect returns a zero Value.
// If v is not a pointer, Indirect returns v.
func Indirect(v Value) Value {
if v.Kind() != Pointer {
return v
}
return v.Elem()
}
// arrayAt returns the i-th element of p,
// an array whose elements are eltSize bytes wide.
// The array pointed at by p must have at least i+1 elements:

View File

@@ -4,8 +4,6 @@
package runtime
import "runtime"
// Layout of in-memory per-function information prepared by linker
// See https://golang.org/s/go12symtab.
// Keep in sync with linker (../cmd/link/internal/ld/pcln.go:/pclntab)
@@ -30,10 +28,6 @@ func StopTrace() {
panic("todo: runtime.StopTrace")
}
func ReadMemStats(m *runtime.MemStats) {
panic("todo: runtime.ReadMemStats")
}
func SetMutexProfileFraction(rate int) int {
panic("todo: runtime.SetMutexProfileFraction")
}

View File

@@ -1,8 +1,16 @@
//go:build !nogc
//go:build !nogc && !baremetal
package runtime
import "github.com/goplus/llgo/runtime/internal/clite/bdwgc"
import (
"runtime"
"github.com/goplus/llgo/runtime/internal/clite/bdwgc"
)
func ReadMemStats(m *runtime.MemStats) {
panic("todo: runtime.ReadMemStats")
}
func GC() {
bdwgc.Gcollect()

View File

@@ -0,0 +1,29 @@
//go:build !nogc && baremetal
package runtime
import (
"runtime"
"github.com/goplus/llgo/runtime/internal/runtime/tinygogc"
)
func ReadMemStats(m *runtime.MemStats) {
stats := tinygogc.ReadGCStats()
m.Alloc = stats.Alloc
m.TotalAlloc = stats.TotalAlloc
m.Sys = stats.Sys
m.Mallocs = stats.Mallocs
m.Frees = stats.Frees
m.HeapAlloc = stats.HeapAlloc
m.HeapSys = stats.HeapSys
m.HeapIdle = stats.HeapIdle
m.HeapInuse = stats.HeapInuse
m.StackInuse = stats.StackInuse
m.StackSys = stats.StackSys
m.GCSys = stats.GCSys
}
func GC() {
tinygogc.GC()
}

View File

@@ -140,6 +140,10 @@ type Func struct {
opaque struct{} // unexported field to disallow conversions
}
func (f *Func) Name() string {
panic("todo")
}
// moduledata records information about the layout of the executable
// image. It is written by the linker. Any changes here must be
// matched changes to the code in cmd/link/internal/ld/symtab.go:symtab.

View File

@@ -0,0 +1,40 @@
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package runtime
import "github.com/goplus/llgo/runtime/internal/clite/tls"
var deferTLS = tls.Alloc[*Defer](func(head **Defer) {
if head != nil {
*head = nil
}
})
// SetThreadDefer associates the current thread with the given defer chain.
func SetThreadDefer(head *Defer) {
deferTLS.Set(head)
}
// GetThreadDefer returns the current thread's defer chain head.
func GetThreadDefer() *Defer {
return deferTLS.Get()
}
// ClearThreadDefer resets the current thread's defer chain to nil.
func ClearThreadDefer() {
deferTLS.Clear()
}

View File

@@ -0,0 +1,184 @@
//go:build baremetal || testGC
package tinygogc
import "unsafe"
type GCStats struct {
// General statistics.
// Alloc is bytes of allocated heap objects.
//
// This is the same as HeapAlloc (see below).
Alloc uint64
// TotalAlloc is cumulative bytes allocated for heap objects.
//
// TotalAlloc increases as heap objects are allocated, but
// unlike Alloc and HeapAlloc, it does not decrease when
// objects are freed.
TotalAlloc uint64
// Sys is the total bytes of memory obtained from the OS.
//
// Sys is the sum of the XSys fields below. Sys measures the
// virtual address space reserved by the Go runtime for the
// heap, stacks, and other internal data structures. It's
// likely that not all of the virtual address space is backed
// by physical memory at any given moment, though in general
// it all was at some point.
Sys uint64
// Mallocs is the cumulative count of heap objects allocated.
// The number of live objects is Mallocs - Frees.
Mallocs uint64
// Frees is the cumulative count of heap objects freed.
Frees uint64
// Heap memory statistics.
//
// Interpreting the heap statistics requires some knowledge of
// how Go organizes memory. Go divides the virtual address
// space of the heap into "spans", which are contiguous
// regions of memory 8K or larger. A span may be in one of
// three states:
//
// An "idle" span contains no objects or other data. The
// physical memory backing an idle span can be released back
// to the OS (but the virtual address space never is), or it
// can be converted into an "in use" or "stack" span.
//
// An "in use" span contains at least one heap object and may
// have free space available to allocate more heap objects.
//
// A "stack" span is used for goroutine stacks. Stack spans
// are not considered part of the heap. A span can change
// between heap and stack memory; it is never used for both
// simultaneously.
// HeapAlloc is bytes of allocated heap objects.
//
// "Allocated" heap objects include all reachable objects, as
// well as unreachable objects that the garbage collector has
// not yet freed. Specifically, HeapAlloc increases as heap
// objects are allocated and decreases as the heap is swept
// and unreachable objects are freed. Sweeping occurs
// incrementally between GC cycles, so these two processes
// occur simultaneously, and as a result HeapAlloc tends to
// change smoothly (in contrast with the sawtooth that is
// typical of stop-the-world garbage collectors).
HeapAlloc uint64
// HeapSys is bytes of heap memory obtained from the OS.
//
// HeapSys measures the amount of virtual address space
// reserved for the heap. This includes virtual address space
// that has been reserved but not yet used, which consumes no
// physical memory, but tends to be small, as well as virtual
// address space for which the physical memory has been
// returned to the OS after it became unused (see HeapReleased
// for a measure of the latter).
//
// HeapSys estimates the largest size the heap has had.
HeapSys uint64
// HeapIdle is bytes in idle (unused) spans.
//
// Idle spans have no objects in them. These spans could be
// (and may already have been) returned to the OS, or they can
// be reused for heap allocations, or they can be reused as
// stack memory.
//
// HeapIdle minus HeapReleased estimates the amount of memory
// that could be returned to the OS, but is being retained by
// the runtime so it can grow the heap without requesting more
// memory from the OS. If this difference is significantly
// larger than the heap size, it indicates there was a recent
// transient spike in live heap size.
HeapIdle uint64
// HeapInuse is bytes in in-use spans.
//
// In-use spans have at least one object in them. These spans
// can only be used for other objects of roughly the same
// size.
//
// HeapInuse minus HeapAlloc estimates the amount of memory
// that has been dedicated to particular size classes, but is
// not currently being used. This is an upper bound on
// fragmentation, but in general this memory can be reused
// efficiently.
HeapInuse uint64
// Stack memory statistics.
//
// Stacks are not considered part of the heap, but the runtime
// can reuse a span of heap memory for stack memory, and
// vice-versa.
// StackInuse is bytes in stack spans.
//
// In-use stack spans have at least one stack in them. These
// spans can only be used for other stacks of the same size.
//
// There is no StackIdle because unused stack spans are
// returned to the heap (and hence counted toward HeapIdle).
StackInuse uint64
// StackSys is bytes of stack memory obtained from the OS.
//
// StackSys is StackInuse, plus any memory obtained directly
// from the OS for OS thread stacks.
//
// In non-cgo programs this metric is currently equal to StackInuse
// (but this should not be relied upon, and the value may change in
// the future).
//
// In cgo programs this metric includes OS thread stacks allocated
// directly from the OS. Currently, this only accounts for one stack in
// c-shared and c-archive build modes and other sources of stacks from
// the OS (notably, any allocated by C code) are not currently measured.
// Note this too may change in the future.
StackSys uint64
// GCSys is bytes of memory in garbage collection metadata.
GCSys uint64
}
func ReadGCStats() GCStats {
var heapInuse, heapIdle uint64
lock(&gcMutex)
for block := uintptr(0); block < endBlock; block++ {
bstate := gcStateOf(block)
if bstate == blockStateFree {
heapIdle += uint64(bytesPerBlock)
} else {
heapInuse += uint64(bytesPerBlock)
}
}
stackEnd := uintptr(unsafe.Pointer(&_stackEnd))
stackSys := stackTop - stackEnd
stats := GCStats{
Alloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock),
TotalAlloc: gcTotalAlloc,
Sys: uint64(heapEnd - heapStart),
Mallocs: gcMallocs,
Frees: gcFrees,
HeapAlloc: (gcTotalBlocks - gcFreedBlocks) * uint64(bytesPerBlock),
HeapSys: heapInuse + heapIdle,
HeapIdle: heapIdle,
HeapInuse: heapInuse,
StackInuse: uint64(stackTop - uintptr(getsp())),
StackSys: uint64(stackSys),
GCSys: uint64(heapEnd - uintptr(metadataStart)),
}
unlock(&gcMutex)
return stats
}

View File

@@ -0,0 +1,54 @@
//go:build !testGC
package tinygogc
import (
"unsafe"
_ "unsafe"
)
// LLGoPackage instructs the LLGo linker to wrap C standard library memory allocation
// functions (malloc, realloc, calloc) so they use the tinygogc allocator instead.
// This ensures all memory allocations go through the GC, including C library calls.
const LLGoPackage = "link: --wrap=malloc --wrap=realloc --wrap=calloc"
//export __wrap_malloc
func __wrap_malloc(size uintptr) unsafe.Pointer {
return Alloc(size)
}
//export __wrap_calloc
func __wrap_calloc(nmemb, size uintptr) unsafe.Pointer {
totalSize := nmemb * size
// Check for multiplication overflow
if nmemb != 0 && totalSize/nmemb != size {
return nil // Overflow
}
return Alloc(totalSize)
}
//export __wrap_realloc
func __wrap_realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
return Realloc(ptr, size)
}
//go:linkname getsp llgo.stackSave
func getsp() unsafe.Pointer
//go:linkname _heapStart _heapStart
var _heapStart [0]byte
//go:linkname _heapEnd _heapEnd
var _heapEnd [0]byte
//go:linkname _stackStart _stack_top
var _stackStart [0]byte
//go:linkname _stackEnd _stack_end
var _stackEnd [0]byte
//go:linkname _globals_start _globals_start
var _globals_start [0]byte
//go:linkname _globals_end _globals_end
var _globals_end [0]byte

View File

@@ -0,0 +1,25 @@
//go:build testGC
package tinygogc
import (
_ "unsafe"
)
var currentStack uintptr
func getsp() uintptr {
return currentStack
}
var _heapStart [0]byte
var _heapEnd [0]byte
var _stackStart [0]byte
var _stackEnd [0]byte
var _globals_start [0]byte
var _globals_end [0]byte

View File

@@ -0,0 +1,570 @@
//go:build baremetal || testGC
/*
* Copyright (c) 2018-2025 The TinyGo Authors. All rights reserved.
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Package tinygogc implements a conservative mark-and-sweep garbage collector
// for baremetal environments where the standard Go runtime and bdwgc are unavailable.
//
// This implementation is based on TinyGo's GC and is designed for resource-constrained
// embedded systems. It uses a block-based allocator with conservative pointer scanning.
//
// Build tags:
// - baremetal: Enables this GC for baremetal targets
// - testGC: Enables testing mode with mock implementations
//
// Memory Layout:
// The heap is divided into fixed-size blocks (32 bytes on 64-bit). Metadata is stored
// at the end of the heap, using 2 bits per block to track state (free/head/tail/mark).
package tinygogc
import (
"unsafe"
c "github.com/goplus/llgo/runtime/internal/clite"
)
const gcDebug = false
// blockState stores the four states in which a block can be. It is two bits in
// size.
const (
blockStateFree uint8 = 0 // 00
blockStateHead uint8 = 1 // 01
blockStateTail uint8 = 2 // 10
blockStateMark uint8 = 3 // 11
blockStateMask uint8 = 3 // 11
)
// The byte value of a block where every block is a 'tail' block.
const blockStateByteAllTails = 0 |
uint8(blockStateTail<<(stateBits*3)) |
uint8(blockStateTail<<(stateBits*2)) |
uint8(blockStateTail<<(stateBits*1)) |
uint8(blockStateTail<<(stateBits*0))
var (
heapStart uintptr // start address of heap area
heapEnd uintptr // end address of heap area
globalsStart uintptr // start address of global variable area
globalsEnd uintptr // end address of global variable area
stackTop uintptr // the top of stack
endBlock uintptr // GC end block index
metadataStart unsafe.Pointer // start address of GC metadata
nextAlloc uintptr // the next block that should be tried by the allocator
gcTotalAlloc uint64 // total number of bytes allocated
gcTotalBlocks uint64 // total number of allocated blocks
gcMallocs uint64 // total number of allocations
gcFrees uint64 // total number of objects freed
gcFreedBlocks uint64 // total number of freed blocks
// stackOverflow is a flag which is set when the GC scans too deep while marking.
// After it is set, all marked allocations must be re-scanned.
markStackOverflow bool
// zeroSizedAlloc is just a sentinel that gets returned when allocating 0 bytes.
zeroSizedAlloc uint8
gcMutex mutex // gcMutex protects GC related variables
isGCInit bool // isGCInit indicates GC initialization state
)
// Some globals + constants for the entire GC.
const (
wordsPerBlock = 4 // number of pointers in an allocated block
bytesPerBlock = wordsPerBlock * unsafe.Sizeof(heapStart)
stateBits = 2 // how many bits a block state takes (see blockState type)
blocksPerStateByte = 8 / stateBits
markStackSize = 8 * unsafe.Sizeof((*int)(nil)) // number of to-be-marked blocks to queue before forcing a rescan
)
// this function MUST be initalized first, which means it's required to be initalized before runtime
func initGC() {
// reserve 2K blocks for libc internal malloc, we cannot wrap those internal functions
heapStart = uintptr(unsafe.Pointer(&_heapStart)) + 2048
heapEnd = uintptr(unsafe.Pointer(&_heapEnd))
globalsStart = uintptr(unsafe.Pointer(&_globals_start))
globalsEnd = uintptr(unsafe.Pointer(&_globals_end))
totalSize := heapEnd - heapStart
metadataSize := (totalSize + blocksPerStateByte*bytesPerBlock) / (1 + blocksPerStateByte*bytesPerBlock)
metadataStart = unsafe.Pointer(heapEnd - metadataSize)
endBlock = (uintptr(metadataStart) - heapStart) / bytesPerBlock
stackTop = uintptr(unsafe.Pointer(&_stackStart))
c.Memset(metadataStart, 0, metadataSize)
}
func lazyInit() {
if !isGCInit {
initGC()
isGCInit = true
}
}
func gcPanic(s *c.Char) {
c.Printf(c.Str("%s"), s)
c.Exit(2)
}
// blockFromAddr returns a block given an address somewhere in the heap (which
// might not be heap-aligned).
func blockFromAddr(addr uintptr) uintptr {
if addr < heapStart || addr >= uintptr(metadataStart) {
gcPanic(c.Str("gc: trying to get block from invalid address"))
}
return (addr - heapStart) / bytesPerBlock
}
// Return a pointer to the start of the allocated object.
func gcPointerOf(blockAddr uintptr) unsafe.Pointer {
return unsafe.Pointer(gcAddressOf(blockAddr))
}
// Return the address of the start of the allocated object.
func gcAddressOf(blockAddr uintptr) uintptr {
addr := heapStart + blockAddr*bytesPerBlock
if addr > uintptr(metadataStart) {
gcPanic(c.Str("gc: block pointing inside metadata"))
}
return addr
}
// findHead returns the head (first block) of an object, assuming the block
// points to an allocated object. It returns the same block if this block
// already points to the head.
func gcFindHead(blockAddr uintptr) uintptr {
for {
// Optimization: check whether the current block state byte (which
// contains the state of multiple blocks) is composed entirely of tail
// blocks. If so, we can skip back to the last block in the previous
// state byte.
// This optimization speeds up findHead for pointers that point into a
// large allocation.
stateByte := gcStateByteOf(blockAddr)
if stateByte == blockStateByteAllTails {
blockAddr -= (blockAddr % blocksPerStateByte) + 1
continue
}
// Check whether we've found a non-tail block, which means we found the
// head.
state := gcStateFromByte(blockAddr, stateByte)
if state != blockStateTail {
break
}
blockAddr--
}
if gcStateOf(blockAddr) != blockStateHead && gcStateOf(blockAddr) != blockStateMark {
gcPanic(c.Str("gc: found tail without head"))
}
return blockAddr
}
// findNext returns the first block just past the end of the tail. This may or
// may not be the head of an object.
func gcFindNext(blockAddr uintptr) uintptr {
if gcStateOf(blockAddr) == blockStateHead || gcStateOf(blockAddr) == blockStateMark {
blockAddr++
}
for gcAddressOf(blockAddr) < uintptr(metadataStart) && gcStateOf(blockAddr) == blockStateTail {
blockAddr++
}
return blockAddr
}
func gcStateByteOf(blockAddr uintptr) byte {
return *(*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte))
}
// Return the block state given a state byte. The state byte must have been
// obtained using b.stateByte(), otherwise the result is incorrect.
func gcStateFromByte(blockAddr uintptr, stateByte byte) uint8 {
return uint8(stateByte>>((blockAddr%blocksPerStateByte)*stateBits)) & blockStateMask
}
// State returns the current block state.
func gcStateOf(blockAddr uintptr) uint8 {
return gcStateFromByte(blockAddr, gcStateByteOf(blockAddr))
}
// setState sets the current block to the given state, which must contain more
// bits than the current state. Allowed transitions: from free to any state and
// from head to mark.
func gcSetState(blockAddr uintptr, newState uint8) {
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte))
*stateBytePtr |= uint8(newState << ((blockAddr % blocksPerStateByte) * stateBits))
if gcStateOf(blockAddr) != newState {
gcPanic(c.Str("gc: setState() was not successful"))
}
}
// markFree sets the block state to free, no matter what state it was in before.
func gcMarkFree(blockAddr uintptr) {
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte))
*stateBytePtr &^= uint8(blockStateMask << ((blockAddr % blocksPerStateByte) * stateBits))
if gcStateOf(blockAddr) != blockStateFree {
gcPanic(c.Str("gc: markFree() was not successful"))
}
*(*[wordsPerBlock]uintptr)(unsafe.Pointer(gcAddressOf(blockAddr))) = [wordsPerBlock]uintptr{}
}
// unmark changes the state of the block from mark to head. It must be marked
// before calling this function.
func gcUnmark(blockAddr uintptr) {
if gcStateOf(blockAddr) != blockStateMark {
gcPanic(c.Str("gc: unmark() on a block that is not marked"))
}
clearMask := blockStateMask ^ blockStateHead // the bits to clear from the state
stateBytePtr := (*uint8)(unsafe.Add(metadataStart, blockAddr/blocksPerStateByte))
*stateBytePtr &^= uint8(clearMask << ((blockAddr % blocksPerStateByte) * stateBits))
if gcStateOf(blockAddr) != blockStateHead {
gcPanic(c.Str("gc: unmark() was not successful"))
}
}
func isOnHeap(ptr uintptr) bool {
return ptr >= heapStart && ptr < uintptr(metadataStart)
}
func isPointer(ptr uintptr) bool {
// TODO: implement precise GC
return isOnHeap(ptr)
}
// alloc tries to find some free space on the heap, possibly doing a garbage
// collection cycle if needed. If no space is free, it panics.
//
//go:noinline
func Alloc(size uintptr) unsafe.Pointer {
if size == 0 {
return unsafe.Pointer(&zeroSizedAlloc)
}
lock(&gcMutex)
lazyInit()
gcTotalAlloc += uint64(size)
gcMallocs++
neededBlocks := (size + (bytesPerBlock - 1)) / bytesPerBlock
gcTotalBlocks += uint64(neededBlocks)
// Continue looping until a run of free blocks has been found that fits the
// requested size.
index := nextAlloc
numFreeBlocks := uintptr(0)
heapScanCount := uint8(0)
for {
if index == nextAlloc {
if heapScanCount == 0 {
heapScanCount = 1
} else if heapScanCount == 1 {
// The entire heap has been searched for free memory, but none
// could be found. Run a garbage collection cycle to reclaim
// free memory and try again.
heapScanCount = 2
freeBytes := gc()
heapSize := uintptr(metadataStart) - heapStart
if freeBytes < heapSize/3 {
// Ensure there is at least 33% headroom.
// This percentage was arbitrarily chosen, and may need to
// be tuned in the future.
growHeap()
}
} else {
// Even after garbage collection, no free memory could be found.
// Try to increase heap size.
if growHeap() {
// Success, the heap was increased in size. Try again with a
// larger heap.
} else {
// Unfortunately the heap could not be increased. This
// happens on baremetal systems for example (where all
// available RAM has already been dedicated to the heap).
gcPanic(c.Str("out of memory"))
}
}
}
// Wrap around the end of the heap.
if index == endBlock {
index = 0
// Reset numFreeBlocks as allocations cannot wrap.
numFreeBlocks = 0
// In rare cases, the initial heap might be so small that there are
// no blocks at all. In this case, it's better to jump back to the
// start of the loop and try again, until the GC realizes there is
// no memory and grows the heap.
// This can sometimes happen on WebAssembly, where the initial heap
// is created by whatever is left on the last memory page.
continue
}
// Is the block we're looking at free?
if gcStateOf(index) != blockStateFree {
// This block is in use. Try again from this point.
numFreeBlocks = 0
index++
continue
}
numFreeBlocks++
index++
// Are we finished?
if numFreeBlocks == neededBlocks {
// Found a big enough range of free blocks!
nextAlloc = index
thisAlloc := index - neededBlocks
// Set the following blocks as being allocated.
gcSetState(thisAlloc, blockStateHead)
for i := thisAlloc + 1; i != nextAlloc; i++ {
gcSetState(i, blockStateTail)
}
unlock(&gcMutex)
// Return a pointer to this allocation.
return c.Memset(gcPointerOf(thisAlloc), 0, size)
}
}
}
func Realloc(ptr unsafe.Pointer, size uintptr) unsafe.Pointer {
if ptr == nil {
return Alloc(size)
}
lock(&gcMutex)
lazyInit()
unlock(&gcMutex)
ptrAddress := uintptr(ptr)
endOfTailAddress := gcAddressOf(gcFindNext(blockFromAddr(ptrAddress)))
// this might be a few bytes longer than the original size of
// ptr, because we align to full blocks of size bytesPerBlock
oldSize := endOfTailAddress - ptrAddress
if size <= oldSize {
return ptr
}
newAlloc := Alloc(size)
c.Memcpy(newAlloc, ptr, oldSize)
free(ptr)
return newAlloc
}
func free(ptr unsafe.Pointer) {
// TODO: free blocks on request, when the compiler knows they're unused.
}
func GC() uintptr {
lock(&gcMutex)
freeBytes := gc()
unlock(&gcMutex)
return freeBytes
}
// runGC performs a garbage collection cycle. It is the internal implementation
// of the runtime.GC() function. The difference is that it returns the number of
// free bytes in the heap after the GC is finished.
func gc() (freeBytes uintptr) {
lazyInit()
if gcDebug {
println("running collection cycle...")
}
// Mark phase: mark all reachable objects, recursively.
gcMarkReachable()
finishMark()
// If we're using threads, resume all other threads before starting the
// sweep.
gcResumeWorld()
// Sweep phase: free all non-marked objects and unmark marked objects for
// the next collection cycle.
freeBytes = sweep()
return
}
// markRoots reads all pointers from start to end (exclusive) and if they look
// like a heap pointer and are unmarked, marks them and scans that object as
// well (recursively). The start and end parameters must be valid pointers and
// must be aligned.
func markRoots(start, end uintptr) {
if start >= end {
gcPanic(c.Str("gc: unexpected range to mark"))
}
// Reduce the end bound to avoid reading too far on platforms where pointer alignment is smaller than pointer size.
// If the size of the range is 0, then end will be slightly below start after this.
end -= unsafe.Sizeof(end) - unsafe.Alignof(end)
for addr := start; addr < end; addr += unsafe.Alignof(addr) {
root := *(*uintptr)(unsafe.Pointer(addr))
markRoot(addr, root)
}
}
// startMark starts the marking process on a root and all of its children.
func startMark(root uintptr) {
var stack [markStackSize]uintptr
stack[0] = root
gcSetState(root, blockStateMark)
stackLen := 1
for stackLen > 0 {
// Pop a block off of the stack.
stackLen--
block := stack[stackLen]
start, end := gcAddressOf(block), gcAddressOf(gcFindNext(block))
for addr := start; addr != end; addr += unsafe.Alignof(addr) {
// Load the word.
word := *(*uintptr)(unsafe.Pointer(addr))
if !isPointer(word) {
// Not a heap pointer.
continue
}
// Find the corresponding memory block.
referencedBlock := blockFromAddr(word)
if gcStateOf(referencedBlock) == blockStateFree {
// The to-be-marked object doesn't actually exist.
// This is probably a false positive.
continue
}
// Move to the block's head.
referencedBlock = gcFindHead(referencedBlock)
if gcStateOf(referencedBlock) == blockStateMark {
// The block has already been marked by something else.
continue
}
// Mark block.
gcSetState(referencedBlock, blockStateMark)
if stackLen == len(stack) {
// The stack is full.
// It is necessary to rescan all marked blocks once we are done.
markStackOverflow = true
if gcDebug {
println("gc stack overflowed")
}
continue
}
// Push the pointer onto the stack to be scanned later.
stack[stackLen] = referencedBlock
stackLen++
}
}
}
// finishMark finishes the marking process by processing all stack overflows.
func finishMark() {
for markStackOverflow {
// Re-mark all blocks.
markStackOverflow = false
for block := uintptr(0); block < endBlock; block++ {
if gcStateOf(block) != blockStateMark {
// Block is not marked, so we do not need to rescan it.
continue
}
// Re-mark the block.
startMark(block)
}
}
}
// mark a GC root at the address addr.
func markRoot(addr, root uintptr) {
if isOnHeap(root) {
block := blockFromAddr(root)
if gcStateOf(block) == blockStateFree {
// The to-be-marked object doesn't actually exist.
// This could either be a dangling pointer (oops!) but most likely
// just a false positive.
return
}
head := gcFindHead(block)
if gcStateOf(head) != blockStateMark {
startMark(head)
}
}
}
// Sweep goes through all memory and frees unmarked
// It returns how many bytes are free in the heap after the sweep.
func sweep() (freeBytes uintptr) {
freeCurrentObject := false
var freed uint64
for block := uintptr(0); block < endBlock; block++ {
switch gcStateOf(block) {
case blockStateHead:
// Unmarked head. Free it, including all tail blocks following it.
gcMarkFree(block)
freeCurrentObject = true
gcFrees++
freed++
case blockStateTail:
if freeCurrentObject {
// This is a tail object following an unmarked head.
// Free it now.
gcMarkFree(block)
freed++
}
case blockStateMark:
// This is a marked object. The next tail blocks must not be freed,
// but the mark bit must be removed so the next GC cycle will
// collect this object if it is unreferenced then.
gcUnmark(block)
freeCurrentObject = false
case blockStateFree:
freeBytes += bytesPerBlock
}
}
gcFreedBlocks += freed
freeBytes += uintptr(freed) * bytesPerBlock
return
}
// growHeap tries to grow the heap size. It returns true if it succeeds, false
// otherwise.
func growHeap() bool {
// On baremetal, there is no way the heap can be grown.
return false
}
func gcMarkReachable() {
markRoots(uintptr(getsp()), stackTop)
markRoots(globalsStart, globalsEnd)
}
func gcResumeWorld() {
// Nothing to do here (single threaded).
}

View File

@@ -0,0 +1,8 @@
package tinygogc
// TODO(MeteorsLiu): mutex lock for baremetal GC
type mutex struct{}
func lock(m *mutex) {}
func unlock(m *mutex) {}

View File

@@ -0,0 +1,604 @@
//go:build testGC
package tinygogc
import (
"testing"
"unsafe"
c "github.com/goplus/llgo/runtime/internal/clite"
)
const (
// Mock a typical embedded system with 128KB RAM
mockHeapSize = 128 * 1024 // 128KB
mockGlobalsSize = 4 * 1024 // 4KB for globals
mockStackSize = 8 * 1024 // 8KB for stack
mockReservedSize = 2048 // 2KB reserved as in real implementation
)
type testObject struct {
data [4]uintptr
}
// mockGCEnv provides a controlled root environment for GC testing
type mockGCEnv struct {
memory []byte
heapStart uintptr
heapEnd uintptr
globalsStart uintptr
globalsEnd uintptr
stackStart uintptr
stackEnd uintptr
// Controlled root sets for testing
rootObjects []unsafe.Pointer
// Original GC state to restore
originalHeapStart uintptr
originalHeapEnd uintptr
originalGlobalsStart uintptr
originalGlobalsEnd uintptr
originalStackTop uintptr
originalEndBlock uintptr
originalMetadataStart unsafe.Pointer
originalNextAlloc uintptr
originalIsGCInit bool
// Mock mode flag
mockMode bool
}
// createMockGCEnv creates a completely isolated GC environment
func createMockGCEnv() *mockGCEnv {
totalMemory := mockHeapSize + mockGlobalsSize + mockStackSize
memory := make([]byte, totalMemory)
baseAddr := uintptr(unsafe.Pointer(&memory[0]))
env := &mockGCEnv{
memory: memory,
globalsStart: baseAddr,
globalsEnd: baseAddr + mockGlobalsSize,
heapStart: baseAddr + mockGlobalsSize + mockReservedSize,
heapEnd: baseAddr + mockGlobalsSize + mockHeapSize,
stackStart: baseAddr + mockGlobalsSize + mockHeapSize,
stackEnd: baseAddr + uintptr(totalMemory),
rootObjects: make([]unsafe.Pointer, 0),
mockMode: false,
}
return env
}
// setupMockGC initializes the GC with mock memory layout using initGC's logic
func (env *mockGCEnv) setupMockGC() {
// Save original GC state
env.originalHeapStart = heapStart
env.originalHeapEnd = heapEnd
env.originalGlobalsStart = globalsStart
env.originalGlobalsEnd = globalsEnd
env.originalStackTop = stackTop
env.originalEndBlock = endBlock
env.originalMetadataStart = metadataStart
env.originalNextAlloc = nextAlloc
env.originalIsGCInit = isGCInit
// Set currentStack for getsp()
currentStack = env.stackStart
// Apply initGC's logic with our mock memory layout
// This is the same logic as initGC() but with our mock addresses
heapStart = env.heapStart + 2048 // reserve 2K blocks like initGC does
heapEnd = env.heapEnd
globalsStart = env.globalsStart
globalsEnd = env.globalsEnd
stackTop = env.stackEnd
totalSize := heapEnd - heapStart
metadataSize := (totalSize + blocksPerStateByte*bytesPerBlock) / (1 + blocksPerStateByte*bytesPerBlock)
metadataStart = unsafe.Pointer(heapEnd - metadataSize)
endBlock = (uintptr(metadataStart) - heapStart) / bytesPerBlock
// Clear metadata using memset like initGC does
c.Memset(metadataStart, 0, metadataSize)
// Reset allocator state and all GC statistics for clean test environment
nextAlloc = 0
isGCInit = true
// Reset all GC statistics to start from clean state
gcTotalAlloc = 0
gcTotalBlocks = 0
gcMallocs = 0
gcFrees = 0
gcFreedBlocks = 0
markStackOverflow = false
}
// restoreOriginalGC restores the original GC state
func (env *mockGCEnv) restoreOriginalGC() {
heapStart = env.originalHeapStart
heapEnd = env.originalHeapEnd
globalsStart = env.originalGlobalsStart
globalsEnd = env.originalGlobalsEnd
stackTop = env.originalStackTop
endBlock = env.originalEndBlock
metadataStart = env.originalMetadataStart
nextAlloc = env.originalNextAlloc
isGCInit = false
}
// enableMockMode enables mock root scanning mode
func (env *mockGCEnv) enableMockMode() {
env.mockMode = true
}
// disableMockMode disables mock root scanning mode
func (env *mockGCEnv) disableMockMode() {
env.mockMode = false
}
// addRoot adds an object to the controlled root set
func (env *mockGCEnv) addRoot(ptr unsafe.Pointer) {
env.rootObjects = append(env.rootObjects, ptr)
}
// clearRoots removes all objects from the controlled root set
func (env *mockGCEnv) clearRoots() {
env.rootObjects = env.rootObjects[:0]
}
// mockMarkReachable replaces gcMarkReachable when in mock mode
func (env *mockGCEnv) mockMarkReachable() {
if !env.mockMode {
// Use original logic
markRoots(uintptr(getsp()), stackTop)
markRoots(globalsStart, globalsEnd)
return
}
// Mock mode: only scan our controlled roots
for _, root := range env.rootObjects {
addr := uintptr(root)
markRoot(addr, addr)
}
}
// runMockGC runs standard GC but with controlled root scanning
func (env *mockGCEnv) runMockGC() uintptr {
lock(&gcMutex)
defer unlock(&gcMutex)
lazyInit()
if gcDebug {
println("running mock collection cycle...")
}
// Mark phase: use our mock root scanning
env.mockMarkReachable()
finishMark()
// Resume world (no-op in single threaded)
gcResumeWorld()
// Sweep phase: use standard sweep logic
return sweep()
}
// createTestObjects creates a network of objects for testing reachability
func createTestObjects(env *mockGCEnv) []*testObject {
// Allocate several test objects
objects := make([]*testObject, 0, 10)
// Dependencies Graph
// root1 -> child1 -> grandchild1 -> child2
// root1 -> child2 -> grandchild1
// Create root objects (reachable from stack/globals)
root1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
root2 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
objects = append(objects, root1, root2)
// Create objects reachable from root1
child1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
child2 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
root1.data[0] = uintptr(unsafe.Pointer(child1))
root1.data[1] = uintptr(unsafe.Pointer(child2))
objects = append(objects, child1, child2)
// Create objects reachable from child1
grandchild1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
child1.data[0] = uintptr(unsafe.Pointer(grandchild1))
objects = append(objects, grandchild1)
// Create circular reference between child2 and grandchild1
child2.data[0] = uintptr(unsafe.Pointer(grandchild1))
grandchild1.data[0] = uintptr(unsafe.Pointer(child2))
// Create unreachable objects (garbage)
garbage1 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
garbage2 := (*testObject)(Alloc(unsafe.Sizeof(testObject{})))
// Create circular reference in garbage
garbage1.data[0] = uintptr(unsafe.Pointer(garbage2))
garbage2.data[0] = uintptr(unsafe.Pointer(garbage1))
objects = append(objects, garbage1, garbage2)
return objects
}
func TestMockGCBasicAllocation(t *testing.T) {
env := createMockGCEnv()
env.setupMockGC()
defer env.restoreOriginalGC()
// Test basic allocation
ptr1 := Alloc(32)
if ptr1 == nil {
t.Fatal("Failed to allocate 32 bytes")
}
ptr2 := Alloc(64)
if ptr2 == nil {
t.Fatal("Failed to allocate 64 bytes")
}
// Verify pointers are within heap bounds
addr1 := uintptr(ptr1)
addr2 := uintptr(ptr2)
if addr1 < heapStart || addr1 >= uintptr(metadataStart) {
t.Errorf("ptr1 %x not within heap bounds [%x, %x)", addr1, heapStart, uintptr(metadataStart))
}
if addr2 < heapStart || addr2 >= uintptr(metadataStart) {
t.Errorf("ptr2 %x not within heap bounds [%x, %x)", addr2, heapStart, uintptr(metadataStart))
}
t.Logf("Allocated ptr1 at %x, ptr2 at %x", addr1, addr2)
t.Logf("Heap bounds: [%x, %x)", heapStart, uintptr(metadataStart))
}
func TestMockGCReachabilityAndSweep(t *testing.T) {
env := createMockGCEnv()
env.setupMockGC()
defer env.restoreOriginalGC()
// Track initial stats
initialMallocs := gcMallocs
initialFrees := gcFrees
// Create test object network
objects := createTestObjects(env)
// Add first 2 objects as roots using mock control
env.enableMockMode()
env.addRoot(unsafe.Pointer(objects[0])) // root1
env.addRoot(unsafe.Pointer(objects[1])) // root2
t.Logf("Created %d objects, 2 are roots", len(objects))
t.Logf("Mallocs: %d", gcMallocs-initialMallocs)
// Verify all objects are initially allocated
for i, obj := range objects {
addr := uintptr(unsafe.Pointer(obj))
block := blockFromAddr(addr)
state := gcStateOf(block)
if state != blockStateHead {
t.Errorf("Object %d at %x has state %d, expected %d (HEAD)", i, addr, state, blockStateHead)
}
}
// Perform GC with controlled root scanning
freedBytes := env.runMockGC()
t.Logf("Freed %d bytes during GC", freedBytes)
t.Logf("Frees: %d (delta: %d)", gcFrees, gcFrees-initialFrees)
// Verify reachable objects are still allocated
reachableObjects := []unsafe.Pointer{
unsafe.Pointer(objects[0]), // root1
unsafe.Pointer(objects[1]), // root2
unsafe.Pointer(objects[2]), // child1 (reachable from root1)
unsafe.Pointer(objects[3]), // child2 (reachable from root1)
unsafe.Pointer(objects[4]), // grandchild1 (reachable from child1, child2)
}
for i, obj := range reachableObjects {
addr := uintptr(obj)
block := blockFromAddr(addr)
state := gcStateOf(block)
if state != blockStateHead {
t.Errorf("Reachable object %d at %x has state %d, expected %d (HEAD)", i, addr, state, blockStateHead)
}
}
// Verify unreachable objects are freed
unreachableObjects := []unsafe.Pointer{
unsafe.Pointer(objects[5]), // garbage1
unsafe.Pointer(objects[6]), // garbage2
}
for i, obj := range unreachableObjects {
addr := uintptr(obj)
block := blockFromAddr(addr)
state := gcStateOf(block)
if state != blockStateFree {
t.Errorf("Unreachable object %d at %x has state %d, expected %d (FREE)", i, addr, state, blockStateFree)
}
}
// Verify some memory was actually freed
if freedBytes == 0 {
t.Error("Expected some memory to be freed, but freed 0 bytes")
}
if gcFrees == initialFrees {
t.Error("Expected some objects to be freed, but free count didn't change")
}
// Clear refs to make grandchild1 unreachable
objects[2].data[0] = 0 // child1 -> grandchild1
objects[3].data[0] = 0 // child2 -> grandchild1
// Run GC again with same roots
freedBytes = env.runMockGC()
// child2 should still be reachable (through root1)
blockAddr := blockFromAddr(uintptr(unsafe.Pointer(objects[3])))
state := gcStateOf(blockAddr)
if state != blockStateHead {
t.Errorf("Object child2 at %x has state %d, expected %d (HEAD)", blockAddr, state, blockStateHead)
}
// grandchild1 should now be unreachable and freed
blockAddr = blockFromAddr(uintptr(unsafe.Pointer(objects[4])))
state = gcStateOf(blockAddr)
if state != blockStateFree {
t.Errorf("Object grandchild1 at %x has state %d, expected %d (FREE)", blockAddr, state, blockStateFree)
}
}
func TestMockGCMemoryPressure(t *testing.T) {
env := createMockGCEnv()
env.setupMockGC()
defer env.restoreOriginalGC()
// Calculate available heap space
heapSize := uintptr(metadataStart) - heapStart
blockSize := bytesPerBlock
maxBlocks := heapSize / blockSize
t.Logf("Heap size: %d bytes, Block size: %d bytes, Max blocks: %d",
heapSize, blockSize, maxBlocks)
// Allocate until we trigger GC
var allocations []unsafe.Pointer
allocSize := uintptr(32) // Small allocations
// Allocate about 80% of heap to trigger GC pressure
targetAllocations := int(maxBlocks * 4 / 5) // 80% capacity
for i := 0; i < targetAllocations; i++ {
ptr := Alloc(allocSize)
if ptr == nil {
t.Fatalf("Failed to allocate at iteration %d", i)
}
allocations = append(allocations, ptr)
}
initialMallocs := gcMallocs
t.Logf("Allocated %d objects (%d mallocs total)", len(allocations), initialMallocs)
// Enable mock mode and keep only half the allocations as roots
env.enableMockMode()
keepCount := len(allocations) / 2
for i := 0; i < keepCount; i++ {
env.addRoot(allocations[i])
}
t.Logf("Keeping %d objects as roots, %d should be freed", keepCount, len(allocations)-keepCount)
// Force GC with controlled roots
freeBytes := env.runMockGC()
t.Logf("GC freed %d bytes", freeBytes)
t.Logf("Objects freed: %d", gcFrees)
// Try to allocate more after GC
for i := 0; i < 10; i++ {
ptr := Alloc(allocSize)
if ptr == nil {
t.Fatalf("Failed to allocate after GC at iteration %d", i)
}
}
t.Log("Successfully allocated more objects after GC")
}
func TestMockGCStats(t *testing.T) {
env := createMockGCEnv()
env.setupMockGC()
defer env.restoreOriginalGC()
// Get initial stats
initialStats := ReadGCStats()
t.Logf("Initial stats - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d",
initialStats.Mallocs, initialStats.Frees, initialStats.TotalAlloc, initialStats.Alloc)
// Verify basic system stats
expectedSys := uint64(env.heapEnd - env.heapStart - 2048)
if initialStats.Sys != expectedSys {
t.Errorf("Expected Sys %d, got %d", expectedSys, initialStats.Sys)
}
expectedGCSys := uint64(env.heapEnd - uintptr(metadataStart))
if initialStats.GCSys != expectedGCSys {
t.Errorf("Expected GCSys %d, got %d", expectedGCSys, initialStats.GCSys)
}
// Allocate some objects
var allocations []unsafe.Pointer
allocSize := uintptr(64)
numAllocs := 10
for i := 0; i < numAllocs; i++ {
ptr := Alloc(allocSize)
if ptr == nil {
t.Fatalf("Failed to allocate at iteration %d", i)
}
allocations = append(allocations, ptr)
}
// Check stats after allocation
afterAllocStats := ReadGCStats()
t.Logf("After allocation - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d",
afterAllocStats.Mallocs, afterAllocStats.Frees, afterAllocStats.TotalAlloc, afterAllocStats.Alloc)
// Verify allocation stats increased
if afterAllocStats.Mallocs <= initialStats.Mallocs {
t.Errorf("Expected Mallocs to increase from %d, got %d", initialStats.Mallocs, afterAllocStats.Mallocs)
}
if afterAllocStats.TotalAlloc <= initialStats.TotalAlloc {
t.Errorf("Expected TotalAlloc to increase from %d, got %d", initialStats.TotalAlloc, afterAllocStats.TotalAlloc)
}
if afterAllocStats.Alloc <= initialStats.Alloc {
t.Errorf("Expected Alloc to increase from %d, got %d", initialStats.Alloc, afterAllocStats.Alloc)
}
// Verify Alloc and HeapAlloc are the same
if afterAllocStats.Alloc != afterAllocStats.HeapAlloc {
t.Errorf("Expected Alloc (%d) to equal HeapAlloc (%d)", afterAllocStats.Alloc, afterAllocStats.HeapAlloc)
}
// Perform GC with controlled roots - keep only half the allocations
env.enableMockMode()
keepCount := len(allocations) / 2
for i := 0; i < keepCount; i++ {
env.addRoot(allocations[i])
}
freedBytes := env.runMockGC()
t.Logf("GC freed %d bytes", freedBytes)
// Check stats after GC
afterGCStats := ReadGCStats()
t.Logf("After GC - Mallocs: %d, Frees: %d, TotalAlloc: %d, Alloc: %d",
afterGCStats.Mallocs, afterGCStats.Frees, afterGCStats.TotalAlloc, afterGCStats.Alloc)
// Verify GC stats
if afterGCStats.Frees <= afterAllocStats.Frees {
t.Errorf("Expected Frees to increase from %d, got %d", afterAllocStats.Frees, afterGCStats.Frees)
}
// TotalAlloc should not decrease (cumulative)
if afterGCStats.TotalAlloc != afterAllocStats.TotalAlloc {
t.Errorf("Expected TotalAlloc to remain %d after GC, got %d", afterAllocStats.TotalAlloc, afterGCStats.TotalAlloc)
}
// Alloc should decrease (freed objects)
if afterGCStats.Alloc >= afterAllocStats.Alloc {
t.Errorf("Expected Alloc to decrease from %d after GC, got %d", afterAllocStats.Alloc, afterGCStats.Alloc)
}
// Verify heap statistics consistency
if afterGCStats.HeapSys != afterGCStats.HeapInuse+afterGCStats.HeapIdle {
t.Errorf("Expected HeapSys (%d) to equal HeapInuse (%d) + HeapIdle (%d)",
afterGCStats.HeapSys, afterGCStats.HeapInuse, afterGCStats.HeapIdle)
}
// Verify live objects calculation
expectedLiveObjects := afterGCStats.Mallocs - afterGCStats.Frees
t.Logf("Live objects: %d (Mallocs: %d - Frees: %d)", expectedLiveObjects, afterGCStats.Mallocs, afterGCStats.Frees)
// The number of live objects should be reasonable (we kept half the allocations plus some overhead)
if expectedLiveObjects < uint64(keepCount) {
t.Errorf("Expected at least %d live objects, got %d", keepCount, expectedLiveObjects)
}
// Test stack statistics
if afterGCStats.StackInuse > afterGCStats.StackSys {
t.Errorf("StackInuse (%d) should not exceed StackSys (%d)", afterGCStats.StackInuse, afterGCStats.StackSys)
}
}
func TestMockGCCircularReferences(t *testing.T) {
env := createMockGCEnv()
env.setupMockGC()
defer env.restoreOriginalGC()
type Node struct {
data [3]uintptr
next uintptr
}
// Create a circular linked list
nodes := make([]*Node, 5)
for i := range nodes {
nodes[i] = (*Node)(Alloc(unsafe.Sizeof(Node{})))
nodes[i].data[0] = uintptr(i) // Store index as data
}
// Link them in a circle
for i := range nodes {
nextIdx := (i + 1) % len(nodes)
nodes[i].next = uintptr(unsafe.Pointer(nodes[nextIdx]))
}
t.Logf("Created circular list of %d nodes", len(nodes))
// Initially all should be allocated
for i, node := range nodes {
addr := uintptr(unsafe.Pointer(node))
block := blockFromAddr(addr)
state := gcStateOf(block)
if state != blockStateHead {
t.Errorf("Node %d at %x has state %d, expected %d", i, addr, state, blockStateHead)
}
}
// Test 1: With root references - objects should NOT be freed
env.enableMockMode()
// Add the first node as root (keeps entire circle reachable)
env.addRoot(unsafe.Pointer(nodes[0]))
freeBytes := env.runMockGC()
t.Logf("GC with root reference freed %d bytes", freeBytes)
// All nodes should still be allocated since they're reachable through the root
for i, node := range nodes {
addr := uintptr(unsafe.Pointer(node))
block := blockFromAddr(addr)
state := gcStateOf(block)
if state != blockStateHead {
t.Errorf("Node %d at %x should still be allocated, but has state %d", i, addr, state)
}
}
// Test 2: Without root references - all circular objects should be freed
env.clearRoots() // Remove all root references
freeBytes = env.runMockGC()
t.Logf("GC without roots freed %d bytes", freeBytes)
// All nodes should now be freed since they're not reachable from any roots
expectedFreed := uintptr(len(nodes)) * ((unsafe.Sizeof(Node{}) + bytesPerBlock - 1) / bytesPerBlock) * bytesPerBlock
if freeBytes < expectedFreed {
t.Errorf("Expected at least %d bytes freed, got %d", expectedFreed, freeBytes)
}
// Verify all nodes are actually freed
for i, node := range nodes {
addr := uintptr(unsafe.Pointer(node))
block := blockFromAddr(addr)
state := gcStateOf(block)
if state != blockStateFree {
t.Errorf("Node %d at %x should be freed, but has state %d", i, addr, state)
}
}
// Verify we can allocate new objects in the freed space
newPtr := Alloc(unsafe.Sizeof(Node{}))
if newPtr == nil {
t.Error("Failed to allocate after freeing circular references")
}
}

View File

@@ -0,0 +1,33 @@
//go:build !nogc && !baremetal
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package runtime
import (
"unsafe"
c "github.com/goplus/llgo/runtime/internal/clite"
"github.com/goplus/llgo/runtime/internal/clite/bdwgc"
)
// FreeDeferNode releases a defer argument node allocated from the Boehm heap.
func FreeDeferNode(ptr unsafe.Pointer) {
if ptr != nil {
bdwgc.Free(c.Pointer(ptr))
}
}

View File

@@ -0,0 +1,32 @@
//go:build nogc || baremetal
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package runtime
import (
"unsafe"
c "github.com/goplus/llgo/runtime/internal/clite"
)
// FreeDeferNode releases the defer node when GC integration is disabled.
func FreeDeferNode(ptr unsafe.Pointer) {
if ptr != nil {
c.Free(ptr)
}
}

View File

@@ -365,7 +365,7 @@ func NewItab(inter *InterfaceType, typ *Type) *Itab {
ret.fun[0] = 0
} else {
data := (*uintptr)(c.Advance(ptr, int(itabHdrSize)))
mthds := methods(u, inter.PkgPath_)
mthds := u.Methods()
for i, m := range inter.Methods {
fn := findMethod(mthds, m)
if fn == nil {
@@ -395,13 +395,6 @@ func findMethod(mthds []abi.Method, im abi.Imethod) abi.Text {
return nil
}
func methods(u *abi.UncommonType, from string) []abi.Method {
if u.PkgPath_ == from {
return u.Methods()
}
return u.ExportedMethods()
}
func IfaceType(i iface) *abi.Type {
if i.tab == nil {
return nil

View File

@@ -1,5 +1,4 @@
//go:build !nogc
// +build !nogc
//go:build !nogc && !baremetal
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.

View File

@@ -0,0 +1,35 @@
//go:build !nogc && baremetal
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package runtime
import (
"unsafe"
"github.com/goplus/llgo/runtime/internal/runtime/tinygogc"
)
// AllocU allocates uninitialized memory.
func AllocU(size uintptr) unsafe.Pointer {
return tinygogc.Alloc(size)
}
// AllocZ allocates zero-initialized memory.
func AllocZ(size uintptr) unsafe.Pointer {
return tinygogc.Alloc(size)
}

View File

@@ -109,18 +109,6 @@ const MaxZero = 1024
var ZeroVal [MaxZero]byte
// func init() {
// signal.Signal(c.Int(syscall.SIGSEGV), func(v c.Int) {
// switch syscall.Signal(v) {
// case syscall.SIGSEGV:
// panic(errorString("invalid memory address or nil pointer dereference"))
// default:
// var buf [20]byte
// panic(errorString("unexpected signal value: " + string(itoa(buf[:], uint64(v)))))
// }
// })
// }
// -----------------------------------------------------------------------------
type SigjmpBuf struct {

View File

@@ -0,0 +1,48 @@
//go:build !wasm && !baremetal
/*
* Copyright (c) 2024 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package runtime
import (
c "github.com/goplus/llgo/runtime/internal/clite"
"github.com/goplus/llgo/runtime/internal/clite/signal"
)
const (
// SIGSEGV is signal number 11 on all Unix-like systems (Linux, Darwin, BSD, etc.)
// Using a hardcoded constant avoids importing the syscall package, which would
// introduce dependencies on errors and internal/reflectlite packages that cause
// linking issues in c-shared and c-archive build modes.
SIGSEGV = c.Int(0xb)
)
// This file contains platform-specific runtime initialization for non-wasm targets.
// The SIGSEGV signal handler enables Go-style panic recovery for nil pointer dereferences
// instead of immediate process termination.
//
// For wasm platform compatibility, signal handling is excluded via build tags.
// See PR #1059 for wasm platform requirements.
func init() {
signal.Signal(SIGSEGV, func(v c.Int) {
if v == SIGSEGV {
panic(errorString("invalid memory address or nil pointer dereference"))
}
var buf [20]byte
panic(errorString("unexpected signal value: " + string(itoa(buf[:], uint64(v)))))
})
}

View File

@@ -7,27 +7,11 @@ import (
//go:embed _overlay/runtime/runtime.go
var fakeRuntime string
//go:embed _overlay/go/parser/resolver.go
var go_parser_resolver string
//go:embed _overlay/testing/testing.go
var testing_testing string
//go:embed _overlay/testing/testing_go123.go
var testing_testing_go123 string
//go:embed _overlay/testing/testing_go124.go
var testing_testing_go124 string
//go:embed _overlay/net/textproto/textproto.go
var net_textproto string
var OverlayFiles = map[string]string{
"math/exp_amd64.go": "package math;",
"go/parser/resolver.go": go_parser_resolver,
"testing/testing.go": testing_testing,
"testing/testing_go123.go": testing_testing_go123,
"testing/testing_go124.go": testing_testing_go124,
"net/textproto/textproto.go": net_textproto,
"runtime/runtime.go": fakeRuntime,
}

View File

@@ -294,7 +294,7 @@ func (p Function) NewBuilder() Builder {
// TODO(xsw): Finalize may cause panic, so comment it.
// b.Finalize()
return &aBuilder{b, nil, p, p.Pkg, prog,
make(map[Expr]dbgExpr), make(map[*types.Scope]DIScope)}
make(map[*types.Scope]DIScope)}
}
// HasBody reports whether the function has a body.

View File

@@ -662,14 +662,8 @@ func (b diBuilder) createExpression(ops []uint64) DIExpression {
// Copy to alloca'd memory to get declareable address.
func (b Builder) constructDebugAddr(v Expr) (dbgPtr Expr, dbgVal Expr, exists bool) {
if v, ok := b.dbgVars[v]; ok {
return v.ptr, v.val, true
}
t := v.Type.RawType().Underlying()
dbgPtr, dbgVal = b.doConstructDebugAddr(v, t)
dbgExpr := dbgExpr{dbgPtr, dbgVal}
b.dbgVars[v] = dbgExpr
b.dbgVars[dbgVal] = dbgExpr
return dbgPtr, dbgVal, false
}
@@ -874,11 +868,7 @@ func (b Builder) DebugFunction(f Function, pos token.Position, bodyPos token.Pos
}
func (b Builder) Param(idx int) Expr {
p := b.Func.Param(idx)
if v, ok := b.dbgVars[p]; ok {
return v.val
}
return p
return b.Func.Param(idx)
}
// -----------------------------------------------------------------------------

110
ssa/eh.go
View File

@@ -91,6 +91,16 @@ func (p Program) tySiglongjmp() *types.Signature {
return p.sigljmpTy
}
// func() unsafe.Pointer
func (p Program) tyStacksave() *types.Signature {
if p.stackSaveTy == nil {
paramPtr := types.NewParam(token.NoPos, nil, "", p.VoidPtr().raw.Type)
params := types.NewTuple(paramPtr)
p.stackSaveTy = types.NewSignatureType(nil, nil, nil, nil, params, false)
}
return p.stackSaveTy
}
func (b Builder) AllocaSigjmpBuf() Expr {
prog := b.Prog
n := unsafe.Sizeof(sigjmpbuf{})
@@ -98,6 +108,12 @@ func (b Builder) AllocaSigjmpBuf() Expr {
return b.Alloca(size)
}
// declare ptr @llvm.stacksave.p0()
func (b Builder) StackSave() Expr {
fn := b.Pkg.cFunc("llvm.stacksave", b.Prog.tyStacksave())
return b.InlineCall(fn)
}
func (b Builder) Sigsetjmp(jb, savemask Expr) Expr {
if b.Prog.target.GOARCH == "wasm" {
return b.Setjmp(jb)
@@ -133,10 +149,6 @@ func (b Builder) Longjmp(jb, retval Expr) {
// -----------------------------------------------------------------------------
const (
deferKey = "__llgo_defer"
)
func (p Function) deferInitBuilder() (b Builder, next BasicBlock) {
b = p.NewBuilder()
next = b.setBlockMoveLast(p.blks[0])
@@ -146,7 +158,6 @@ func (p Function) deferInitBuilder() (b Builder, next BasicBlock) {
type aDefer struct {
nextBit int // next defer bit
key Expr // pthread TLS key
data Expr // pointer to runtime.Defer
bitsPtr Expr // pointer to defer bits
rethPtr Expr // next block of Rethrow
@@ -158,30 +169,6 @@ type aDefer struct {
stmts []func(bits Expr)
}
func (p Package) keyInit(name string) {
keyVar := p.VarOf(name)
if keyVar == nil {
return
}
prog := p.Prog
keyVar.InitNil()
keyVar.impl.SetLinkage(llvm.LinkOnceAnyLinkage)
b := p.afterBuilder()
eq := b.BinOp(token.EQL, b.Load(keyVar.Expr), prog.IntVal(0, prog.CInt()))
b.IfThen(eq, func() {
b.pthreadKeyCreate(keyVar.Expr, prog.Nil(prog.VoidPtr()))
})
}
func (p Package) newKey(name string) Global {
return p.NewVarEx(name, p.Prog.CIntPtr())
}
func (b Builder) deferKey() Expr {
return b.Load(b.Pkg.newKey(deferKey).Expr)
}
const (
// 0: addr sigjmpbuf
// 1: bits uintptr
@@ -214,17 +201,19 @@ func (b Builder) getDefer(kind DoAction) *aDefer {
blks := self.MakeBlocks(2)
procBlk, rethrowBlk := blks[0], blks[1]
key := b.deferKey()
zero := prog.Val(uintptr(0))
link := Expr{b.pthreadGetspecific(key).impl, prog.DeferPtr()}
link := b.Call(b.Pkg.rtFunc("GetThreadDefer"))
jb := b.AllocaSigjmpBuf()
ptr := b.aggregateAlloca(prog.Defer(), jb.impl, zero.impl, link.impl, procBlk.Addr().impl)
ptr := b.aggregateAllocU(prog.Defer(), jb.impl, zero.impl, link.impl, procBlk.Addr().impl)
deferData := Expr{ptr, prog.DeferPtr()}
b.pthreadSetspecific(key, deferData)
b.Call(b.Pkg.rtFunc("SetThreadDefer"), deferData)
bitsPtr := b.FieldAddr(deferData, deferBits)
rethPtr := b.FieldAddr(deferData, deferRethrow)
rundPtr := b.FieldAddr(deferData, deferRunDefers)
argsPtr := b.FieldAddr(deferData, deferArgs)
// Initialize the args list so later guards (e.g. DeferAlways/DeferInLoop)
// can safely detect an empty chain without a prior push.
b.Store(argsPtr, prog.Nil(prog.VoidPtr()))
czero := prog.IntVal(0, prog.CInt())
retval := b.Sigsetjmp(jb, czero)
@@ -237,7 +226,6 @@ func (b Builder) getDefer(kind DoAction) *aDefer {
b.If(b.BinOp(token.EQL, retval, czero), next, panicBlk)
self.defer_ = &aDefer{
key: key,
data: deferData,
bitsPtr: bitsPtr,
rethPtr: rethPtr,
@@ -262,8 +250,7 @@ func (b Builder) getDefer(kind DoAction) *aDefer {
// DeferData returns the defer data (*runtime.Defer).
func (b Builder) DeferData() Expr {
key := b.deferKey()
return Expr{b.pthreadGetspecific(key).impl, b.Prog.DeferPtr()}
return b.Call(b.Pkg.rtFunc("GetThreadDefer"))
}
// Defer emits a defer instruction.
@@ -278,14 +265,17 @@ func (b Builder) Defer(kind DoAction, fn Expr, args ...Expr) {
case DeferInCond:
prog = b.Prog
next := self.nextBit
if uintptr(next) >= unsafe.Sizeof(uintptr(0))*8 {
panic("too many conditional defers")
}
self.nextBit++
bits := b.Load(self.bitsPtr)
nextbit = prog.Val(uintptr(1 << next))
b.Store(self.bitsPtr, b.BinOp(token.OR, bits, nextbit))
case DeferAlways:
// nothing to do
default:
panic("todo: DeferInLoop is not supported - " + b.Func.Name())
case DeferInLoop:
// Loop defers rely on a dedicated drain loop inserted below.
}
typ := b.saveDeferArgs(self, fn, args)
self.stmts = append(self.stmts, func(bits Expr) {
@@ -298,6 +288,29 @@ func (b Builder) Defer(kind DoAction, fn Expr, args ...Expr) {
})
case DeferAlways:
b.callDefer(self, typ, fn, args)
case DeferInLoop:
prog := b.Prog
condBlk := b.Func.MakeBlock()
bodyBlk := b.Func.MakeBlock()
exitBlk := b.Func.MakeBlock()
// Control flow:
// condBlk: check argsPtr for non-nil to see if there's work to drain.
// bodyBlk: execute a single defer node, then jump back to condBlk.
// exitBlk: reached when the list is empty (argsPtr == nil).
// This mirrors runtime's linked-list unwinding semantics for loop defers.
// jump to condition check before executing
b.Jump(condBlk)
b.SetBlockEx(condBlk, AtEnd, true)
list := b.Load(self.argsPtr)
has := b.BinOp(token.NEQ, list, prog.Nil(prog.VoidPtr()))
b.If(has, bodyBlk, exitBlk)
b.SetBlockEx(bodyBlk, AtEnd, true)
b.callDefer(self, typ, fn, args)
b.Jump(condBlk)
b.SetBlockEx(exitBlk, AtEnd, true)
}
})
}
@@ -338,7 +351,7 @@ func (b Builder) saveDeferArgs(self *aDefer, fn Expr, args []Expr) Type {
flds[i+offset] = arg.impl
}
typ := prog.Struct(typs...)
ptr := Expr{b.aggregateMalloc(typ, flds...), prog.VoidPtr()}
ptr := Expr{b.aggregateAllocU(typ, flds...), prog.VoidPtr()}
b.Store(self.argsPtr, ptr)
return typ
}
@@ -349,19 +362,28 @@ func (b Builder) callDefer(self *aDefer, typ Type, fn Expr, args []Expr) {
return
}
prog := b.Prog
zero := prog.Nil(prog.VoidPtr())
list := b.Load(self.argsPtr)
has := b.BinOp(token.NEQ, list, zero)
// The guard is required because callDefer is reused by endDefer() after the
// list has been drained. Without this check we would dereference a nil
// pointer when no loop defers were recorded.
b.IfThen(has, func() {
ptr := b.Load(self.argsPtr)
data := b.Load(Expr{ptr.impl, prog.Pointer(typ)})
offset := 1
b.Store(self.argsPtr, Expr{b.getField(data, 0).impl, prog.VoidPtr()})
if fn.kind == vkClosure {
fn = b.getField(data, 1)
callFn := fn
if callFn.kind == vkClosure {
callFn = b.getField(data, 1)
offset++
}
for i := 0; i < len(args); i++ {
args[i] = b.getField(data, i+offset)
}
b.Call(fn, args...)
b.free(ptr)
b.Call(callFn, args...)
b.Call(b.Pkg.rtFunc("FreeDeferNode"), ptr)
})
}
// RunDefers emits instructions to run deferred instructions.
@@ -415,7 +437,7 @@ func (p Function) endDefer(b Builder) {
}
}
link := b.getField(b.Load(self.data), deferLink)
b.pthreadSetspecific(self.key, link)
b.Call(b.Pkg.rtFunc("SetThreadDefer"), link)
b.IndirectJump(b.Load(rundPtr), nexts)
b.SetBlockEx(panicBlk, AtEnd, false) // panicBlk: exec runDefers and rethrow

53
ssa/eh_loop_test.go Normal file
View File

@@ -0,0 +1,53 @@
//go:build !llgo
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package ssa_test
import (
"strings"
"testing"
"github.com/goplus/llgo/ssa"
"github.com/goplus/llgo/ssa/ssatest"
)
func TestDeferInLoopIR(t *testing.T) {
prog := ssatest.NewProgram(t, nil)
pkg := prog.NewPackage("foo", "foo")
callee := pkg.NewFunc("callee", ssa.NoArgsNoRet, ssa.InGo)
cb := callee.MakeBody(1)
cb.Return()
cb.EndBuild()
fn := pkg.NewFunc("main", ssa.NoArgsNoRet, ssa.InGo)
b := fn.MakeBody(1)
fn.SetRecover(fn.MakeBlock())
// Ensure entry block has a terminator like real codegen
b.Return()
b.SetBlockEx(fn.Block(0), ssa.BeforeLast, true)
b.Defer(ssa.DeferInLoop, callee.Expr)
b.EndBuild()
ir := pkg.Module().String()
if !strings.Contains(ir, "icmp ne ptr") {
t.Fatalf("expected loop defer condition in IR, got:\n%s", ir)
}
}

View File

@@ -31,6 +31,19 @@ func (pkg Package) AddGlobalString(name string, value string) {
pkg.NewVarEx(name, prog.Pointer(styp)).Init(Expr{cv, styp})
}
// ConstString creates an SSA expression representing a Go string literal. The
// returned value is backed by an anonymous global constant and can be used to
// initialize package-level variables or other constant contexts that expect a
// Go string value.
func (pkg Package) ConstString(value string) Expr {
prog := pkg.Prog
styp := prog.String()
data := pkg.createGlobalStr(value)
length := prog.IntVal(uint64(len(value)), prog.Uintptr())
cv := llvm.ConstNamedStruct(styp.ll, []llvm.Value{data, length.impl})
return Expr{cv, styp}
}
// Undefined global string var by names
func (pkg Package) Undefined(names ...string) error {
prog := pkg.Prog

View File

@@ -195,6 +195,7 @@ type aProgram struct {
mallocTy *types.Signature
freeTy *types.Signature
memsetInlineTy *types.Signature
stackSaveTy *types.Signature
createKeyTy *types.Signature
getSpecTy *types.Signature
@@ -797,7 +798,6 @@ func (p Package) afterBuilder() Builder {
// AfterInit is called after the package is initialized (init all packages that depends on).
func (p Package) AfterInit(b Builder, ret BasicBlock) {
p.keyInit(deferKey)
doAfterb := p.afterb != nil
doPyLoadModSyms := p.pyHasModSyms()
if doAfterb || doPyLoadModSyms {

View File

@@ -60,6 +60,42 @@ func TestUnsafeString(t *testing.T) {
b.Return()
}
func TestTooManyConditionalDefers(t *testing.T) {
wd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
os.Chdir("../../runtime")
defer os.Chdir(wd)
prog := NewProgram(nil)
prog.SetRuntime(func() *types.Package {
fset := token.NewFileSet()
imp := packages.NewImporter(fset)
pkg, _ := imp.Import(PkgRuntime)
return pkg
})
pkg := prog.NewPackage("foo", "foo")
target := pkg.NewFunc("f", NoArgsNoRet, InGo)
fn := pkg.NewFunc("main", NoArgsNoRet, InGo)
fn.SetRecover(fn.MakeBlock())
b := fn.MakeBody(1)
defer func() {
if r := recover(); r == nil {
t.Fatal("expected panic: too many conditional defers")
} else if r != "too many conditional defers" {
t.Fatalf("unexpected panic: %v", r)
}
}()
b.Return()
for i := 0; i < 65; i++ {
b.Defer(DeferInCond, target.Expr)
}
}
func TestPointerSize(t *testing.T) {
expected := unsafe.Sizeof(uintptr(0))
if size := NewProgram(nil).PointerSize(); size != int(expected) {

View File

@@ -57,11 +57,6 @@ func (p BasicBlock) Addr() Expr {
// -----------------------------------------------------------------------------
type dbgExpr struct {
ptr Expr
val Expr
}
type aBuilder struct {
impl llvm.Builder
blk BasicBlock
@@ -69,7 +64,6 @@ type aBuilder struct {
Pkg Package
Prog Program
dbgVars map[Expr]dbgExpr // save copied address and values for debug info
diScopeCache map[*types.Scope]DIScope // avoid duplicated DILexicalBlock(s)
}

View File

@@ -1,6 +1,4 @@
__stack = ORIGIN(dram_seg) + LENGTH(dram_seg);
__MIN_STACK_SIZE = 0x1000;
_stack_top = __stack;
_heapEnd = ORIGIN(dram_seg) + LENGTH(dram_seg);
/* Default entry point */
ENTRY(_start)
@@ -94,6 +92,12 @@ SECTIONS
_iram_end = .;
} > iram_seg
.stack (NOLOAD) :
{
. += 16K;
__stack = .;
} > dram_seg
/**
* This section is required to skip .iram0.text area because iram0_0_seg and
* dram0_0_seg reflect the same address space on different buses.
@@ -148,7 +152,7 @@ SECTIONS
} > dram_seg
/* Check if data + heap + stack exceeds RAM limit */
ASSERT(_end <= __stack - __MIN_STACK_SIZE, "region DRAM overflowed by .data and .bss sections")
ASSERT(_end <= _heapEnd, "region DRAM overflowed by .data and .bss sections")
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
@@ -193,3 +197,8 @@ SECTIONS
.gnu.attributes 0 : { KEEP (*(.gnu.attributes)) }
/DISCARD/ : { *(.note.GNU-stack) *(.gnu_debuglink) *(.gnu.lto_*) }
}
_globals_start = _data_start;
_globals_end = _end;
_heapStart = _end;
_stack_top = __stack;

View File

@@ -1,5 +1,4 @@
__stack = ORIGIN(dram_seg) + LENGTH(dram_seg);
__MIN_STACK_SIZE = 0x2000;
_heapEnd = ORIGIN(dram_seg) + LENGTH(dram_seg);
ENTRY(_start)
SECTIONS
@@ -26,6 +25,14 @@ SECTIONS
the same address within the page on the next page up. */
. = ALIGN (CONSTANT (MAXPAGESIZE)) - ((CONSTANT (MAXPAGESIZE) - .) & (CONSTANT (MAXPAGESIZE) - 1)); . = DATA_SEGMENT_ALIGN (CONSTANT (MAXPAGESIZE), CONSTANT (COMMONPAGESIZE));
.stack (NOLOAD) :
{
_stack_end = .;
. = ALIGN(16);
. += 16K;
__stack = .;
}
.rodata :
{
@@ -116,7 +123,7 @@ SECTIONS
. = DATA_SEGMENT_END (.);
/* Check if data + heap + stack exceeds RAM limit */
ASSERT(. <= __stack - __MIN_STACK_SIZE, "region DRAM overflowed by .data and .bss sections")
ASSERT(. <= _heapEnd, "region DRAM overflowed by .data and .bss sections")
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
@@ -165,4 +172,7 @@ SECTIONS
_sbss = __bss_start;
_ebss = _end;
_globals_start = _data_start;
_globals_end = _end;
_heapStart = _end;
_stack_top = __stack;

View File

@@ -19,8 +19,8 @@ MEMORY
/* 64k at the end of DRAM, after ROM bootloader stack
* or entire DRAM (for QEMU only)
*/
dram_seg (RW) : org = 0x3FFF0000 ,
len = 0x10000
dram_seg (RW) : org = 0x3ffae000 ,
len = 0x52000
}
INCLUDE "targets/esp32.app.elf.ld";

326
test/defer_test.go Normal file
View File

@@ -0,0 +1,326 @@
/*
* Copyright (c) 2025 The GoPlus Authors (goplus.org). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package test
import (
"reflect"
"strconv"
"testing"
)
// runLoopDefers exercises a defer statement inside a loop and relies on
// defers executing after the loop but before the function returns.
func runLoopDefers() (result []int) {
for i := 0; i < 3; i++ {
v := i
defer func() {
result = append(result, v)
}()
}
return
}
func runLoopDeferCount(n int) (count int) {
for i := 0; i < n; i++ {
defer func() {
count++
}()
}
return
}
func runDeferRecover() (recovered any, ran bool) {
defer func() {
recovered = recover()
ran = true
}()
panic("defer recover sentinel")
}
func runNestedLoopDeferOrder() (order []string) {
outerNestedLoop(&order)
return
}
func outerNestedLoop(order *[]string) {
for i := 0; i < 3; i++ {
v := i
label := "outer:" + strconv.Itoa(v)
defer func(label string) {
*order = append(*order, label)
}(label)
}
middleNestedLoop(order)
}
func middleNestedLoop(order *[]string) {
for i := 0; i < 2; i++ {
v := i
label := "middle:" + strconv.Itoa(v)
defer func(label string) {
*order = append(*order, label)
}(label)
}
innerNestedLoop(order)
}
func innerNestedLoop(order *[]string) {
for i := 0; i < 4; i++ {
v := i
label := "inner:" + strconv.Itoa(v)
defer func(label string) {
*order = append(*order, label)
}(label)
}
}
func TestDeferInLoopOrder(t *testing.T) {
got := runLoopDefers()
want := []int{2, 1, 0}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected defer order: got %v, want %v", got, want)
}
}
func TestDeferLoopStress(t *testing.T) {
const n = 1_000_000
if got := runLoopDeferCount(n); got != n {
t.Fatalf("unexpected count: got %d, want %d", got, n)
}
}
func TestDeferRecoverHandlesPanic(t *testing.T) {
got, ran := runDeferRecover()
want := "defer recover sentinel"
if !ran {
t.Fatalf("recover defer not executed")
}
str, ok := got.(string)
if !ok {
t.Fatalf("recover returned %T, want string", got)
}
if str != want {
t.Fatalf("unexpected recover value: got %q, want %q", str, want)
}
}
func TestNestedDeferLoops(t *testing.T) {
got := runNestedLoopDeferOrder()
want := []string{
"inner:3", "inner:2", "inner:1", "inner:0",
"middle:1", "middle:0",
"outer:2", "outer:1", "outer:0",
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("unexpected nested order: got %v, want %v", got, want)
}
}
func runNestedConditionalDeferWithRecover() (order []string, recovered any) {
defer func() { recovered = recover() }()
nestedCondOuter(&order)
return
}
func nestedCondOuter(order *[]string) {
for i := 0; i < 3; i++ {
v := i
label := "outer:" + strconv.Itoa(v)
if v%2 == 0 {
defer func(label string) {
*order = append(*order, label)
}(label)
}
nestedCondMiddle(order, v)
}
}
func nestedCondMiddle(order *[]string, v int) {
for j := 0; j < 3; j++ {
u := j
label := "middle:" + strconv.Itoa(u)
if u < 2 {
defer func(label string) {
*order = append(*order, label)
}(label)
}
nestedCondInner(order)
}
if v == 1 {
panic("nested-conditional-boom")
}
}
func nestedCondInner(order *[]string) {
for k := 0; k < 2; k++ {
w := k
label := "inner:" + strconv.Itoa(w)
defer func(label string) {
*order = append(*order, label)
}(label)
}
}
func TestNestedConditionalDeferWithRecover(t *testing.T) {
gotOrder, gotRecovered := runNestedConditionalDeferWithRecover()
wantRecovered := "nested-conditional-boom"
if s, ok := gotRecovered.(string); !ok || s != wantRecovered {
t.Fatalf("unexpected recover value: got %v, want %q", gotRecovered, wantRecovered)
}
wantOrder := []string{
"inner:1", "inner:0",
"inner:1", "inner:0",
"inner:1", "inner:0",
"middle:1", "middle:0",
"inner:1", "inner:0",
"inner:1", "inner:0",
"inner:1", "inner:0",
"middle:1", "middle:0",
"outer:0",
}
if !reflect.DeepEqual(gotOrder, wantOrder) {
t.Fatalf("unexpected nested conditional order: got %v, want %v", gotOrder, wantOrder)
}
}
func callWithRecover(fn func()) (recovered any) {
defer func() { recovered = recover() }()
fn()
return
}
func loopBranchEven(order *[]string, i int) {
label := "even:" + strconv.Itoa(i)
defer func() { *order = append(*order, label) }()
}
func loopBranchOddNoRecover(order *[]string, i int) {
label := "odd-wrap:" + strconv.Itoa(i)
defer func() { *order = append(*order, label) }()
panic("odd-no-recover")
}
func loopBranchOddLocalRecover(order *[]string, i int) {
label := "odd-local:" + strconv.Itoa(i)
defer func() { *order = append(*order, label) }()
defer func() { _ = recover() }()
panic("odd-local-recover")
}
func runLoopBranchRecoverMixed(n int) (order []string, recoveredVals []any) {
for i := 0; i < n; i++ {
if i%2 == 0 {
loopBranchEven(&order, i)
} else if i%4 == 3 {
rec := callWithRecover(func() { loopBranchOddNoRecover(&order, i) })
recoveredVals = append(recoveredVals, rec)
} else {
loopBranchOddLocalRecover(&order, i)
}
}
return
}
func TestLoopBranchRecoverMixed(t *testing.T) {
order, recovered := runLoopBranchRecoverMixed(6)
wantOrder := []string{
"even:0",
"odd-local:1",
"even:2",
"odd-wrap:3",
"even:4",
"odd-local:5",
}
if !reflect.DeepEqual(order, wantOrder) {
t.Fatalf("unexpected loop/branch order: got %v, want %v", order, wantOrder)
}
if len(recovered) != 1 {
t.Fatalf("unexpected recovered count: got %d, want %d", len(recovered), 1)
}
if s, ok := recovered[0].(string); !ok || s != "odd-no-recover" {
t.Fatalf("unexpected recovered value: got %v, want %q", recovered[0], "odd-no-recover")
}
}
func deepInner(order *[]string) {
for i := 0; i < 2; i++ {
idx := i
label := "inner:" + strconv.Itoa(idx)
defer func(label string) {
*order = append(*order, label)
}(label)
if idx == 0 {
continue
}
panic("deep-boom")
}
}
func deepMiddle(order *[]string) {
for i := 0; i < 2; i++ {
idx := i
label := "middle:" + strconv.Itoa(idx)
defer func(label string) {
*order = append(*order, label)
}(label)
if idx == 0 {
continue
}
defer func() {
if rec := recover(); rec != nil {
panic(rec)
}
}()
deepInner(order)
}
}
func deepOuter(order *[]string) (recovered any) {
for i := 0; i < 2; i++ {
idx := i
label := "outer:" + strconv.Itoa(idx)
defer func(label string) {
*order = append(*order, label)
}(label)
if idx == 0 {
continue
}
defer func() {
if rec := recover(); rec != nil {
recovered = rec
}
}()
deepMiddle(order)
}
return
}
func TestPanicCrossTwoFunctionsRecover(t *testing.T) {
var order []string
recovered := deepOuter(&order)
if s, ok := recovered.(string); !ok || s != "deep-boom" {
t.Fatalf("unexpected recovered value: got %v, want %q", recovered, "deep-boom")
}
wantOrder := []string{
"inner:1", "inner:0",
"middle:1", "middle:0",
"outer:1", "outer:0",
}
if !reflect.DeepEqual(order, wantOrder) {
t.Fatalf("unexpected cross-function defer order: got %v, want %v", order, wantOrder)
}
}

View File

@@ -17,9 +17,11 @@
package llvm
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"sort"
"strings"
"github.com/goplus/llgo/internal/env"
@@ -108,4 +110,62 @@ func (e *Env) InstallNameTool() *install_name_tool.Cmd {
return install_name_tool.New(bin)
}
// Readelf returns a command to execute llvm-readelf with given arguments.
func (e *Env) Readelf(args ...string) (*exec.Cmd, error) {
path, err := e.toolPath("llvm-readelf")
if err != nil {
return nil, err
}
return exec.Command(path, args...), nil
}
func (e *Env) toolPath(base string) (string, error) {
if tool := searchTool(e.binDir, base); tool != "" {
return tool, nil
}
if tool, err := exec.LookPath(base); err == nil {
return tool, nil
}
if tool := searchToolInPath(base); tool != "" {
return tool, nil
}
return "", fmt.Errorf("%s not found", base)
}
func searchTool(dir, base string) string {
if dir == "" {
return ""
}
candidate := filepath.Join(dir, base)
if isExecutable(candidate) {
return candidate
}
pattern := filepath.Join(dir, base+"-*")
matches, _ := filepath.Glob(pattern)
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
for _, match := range matches {
if isExecutable(match) {
return match
}
}
return ""
}
func searchToolInPath(base string) string {
for _, dir := range filepath.SplitList(os.Getenv("PATH")) {
if tool := searchTool(dir, base); tool != "" {
return tool
}
}
return ""
}
func isExecutable(path string) bool {
if path == "" {
return false
}
info, err := os.Stat(path)
return err == nil && !info.IsDir()
}
// -----------------------------------------------------------------------------