Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,14 @@ MATLAB, Mathematica and Maple, where each piece lives, and how far it has got.
- [Julia runtime (M3)](bib/julia-runtime.md) — the Core runtime against julia v1.12.6: what
`interpreter.c` executes, the contract with the lowered IR, 63 builtins, 93 intrinsics,
subtyping and dispatch, package layout, exit tests; corrects the counts of the plan above
- [C and C++](bib/c-cpp.md) — feasibility plan for go-embedded-c and go-embedded-cpp: C cells
on the cc/v4 front end compiled to wasm32 and run on wazero or the browser engine, with
wasi-libc; C++ cells as clang-repl compiled to wasm on the same runtime, behind a measured gate
- [template](bib/TEMPLATE.md) for a new domain

`go run ./tools/checklinks` checks that every relative link in the tree resolves; CI runs it.
`go run ./tools/jlcount <julia-source-root>` counts the builtins, intrinsics and core C lines of a
julia source tree, the figures `bib/julia-runtime.md` quotes.
`go run ./tools/srccount [--ext .c,.h] [--contains text] <path>...` counts files, lines and bytes
under source paths, and `go run ./tools/wasmimports <file.wasm>` validates a module with wazero
and lists what its host must provide; `bib/c-cpp.md` quotes both.
389 changes: 389 additions & 0 deletions bib/c-cpp.md

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@ module github.com/go-scikit/roadmap

go 1.27

require (
github.com/spf13/cobra v1.10.2
github.com/tetratelabs/wazero v1.12.0
)

require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/spf13/cobra v1.10.2 // indirect
github.com/spf13/pflag v1.0.9 // indirect
golang.org/x/sys v0.44.0 // indirect
)
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
136 changes: 136 additions & 0 deletions tools/srccount/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
// Command srccount counts, under each given path, the regular files whose
// extension is in --ext (all files when --ext is empty), their lines and
// bytes, and, with --contains, the lines that contain a substring.
// Directories named .git are skipped. bib/c-cpp.md quotes its output.
//
// go run ./tools/srccount --ext .cpp,.h clang/lib/Sema
// go run ./tools/srccount --contains 'gcc.c-torture/execute/' known_failures.go
package main

import (
"bytes"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"

"github.com/spf13/cobra"
)

// osExit is replaced by tests.
var osExit = os.Exit

// Count is the tally for one path.
type Count struct {
Files, Lines, Bytes, Matches int
}

// countLines returns the number of newline-terminated lines of b, plus one
// for a final unterminated line.
func countLines(b []byte) int {
n := bytes.Count(b, []byte{'\n'})
if len(b) > 0 && b[len(b)-1] != '\n' {
n++
}
return n
}

// countMatches returns the number of lines of b that contain sub.
func countMatches(b []byte, sub string) int {
n := 0
for _, line := range strings.Split(string(b), "\n") {
if strings.Contains(line, sub) {
n++
}
}
return n
}

// wanted reports whether name has one of exts; an empty exts accepts all.
func wanted(name string, exts []string) bool {
if len(exts) == 0 {
return true
}
e := filepath.Ext(name)
for _, x := range exts {
if e == x {
return true
}
}
return false
}

// count walks root and tallies the wanted regular files.
func count(root string, exts []string, contains string) (Count, error) {
var c Count
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if d.Name() == ".git" {
return filepath.SkipDir
}
return nil
}
if !d.Type().IsRegular() || !wanted(d.Name(), exts) {
return nil
}
b, err := os.ReadFile(p)
if err != nil {
return err
}
c.Files++
c.Lines += countLines(b)
c.Bytes += len(b)
if contains != "" {
c.Matches += countMatches(b, contains)
}
return nil
})
return c, err
}

func report(w io.Writer, paths, exts []string, contains string) error {
for _, p := range paths {
c, err := count(p, exts, contains)
if err != nil {
return err
}
if c.Files == 0 {
return fmt.Errorf("%s: no file with extension in %v", p, exts)
}
fmt.Fprintf(w, "%s\tfiles %d\tlines %d\tbytes %d", p, c.Files, c.Lines, c.Bytes)
if contains != "" {
fmt.Fprintf(w, "\tcontaining %q %d", contains, c.Matches)
}
fmt.Fprintln(w)
}
return nil
}

// RootCmd is the srccount command.
func RootCmd() *cobra.Command {
var exts []string
var contains string
cmd := &cobra.Command{
Use: "srccount [--ext .c,.h] [--contains text] <path>...",
Short: "Count files, lines and bytes under source paths",
Args: cobra.MinimumNArgs(1),
SilenceUsage: true,
RunE: func(cmd *cobra.Command, args []string) error {
return report(cmd.OutOrStdout(), args, exts, contains)
},
}
cmd.Flags().StringSliceVar(&exts, "ext", nil, "extensions to count, with the dot (default: all files)")
cmd.Flags().StringVar(&contains, "contains", "", "also count the lines containing this text")
return cmd
}

func main() {
if err := RootCmd().Execute(); err != nil {
osExit(1)
}
}
159 changes: 159 additions & 0 deletions tools/srccount/main_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package main

import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)

func TestCountLines(t *testing.T) {
for in, want := range map[string]int{"": 0, "a": 1, "a\n": 1, "a\nb": 2, "\n\n": 2} {
if got := countLines([]byte(in)); got != want {
t.Errorf("countLines(%q) = %d, want %d", in, got, want)
}
}
}

func TestCountMatches(t *testing.T) {
if got := countMatches([]byte("x/execute/a.c\nx/compile/b.c\nexecute/execute/c.c\n"), "execute/"); got != 2 {
t.Fatalf("got %d, want 2 (one per line, not per occurrence)", got)
}
}

func TestWanted(t *testing.T) {
for _, c := range []struct {
name string
exts []string
want bool
}{
{"vector", nil, true},
{"a.cpp", []string{".c", ".cpp"}, true},
{"a.cppm", []string{".cpp"}, false},
{"Makefile", []string{".c"}, false},
} {
if got := wanted(c.name, c.exts); got != c.want {
t.Errorf("wanted(%q, %v) = %v, want %v", c.name, c.exts, got, c.want)
}
}
}

// tree plants files of every kind the walk must accept or skip.
func tree(t *testing.T) string {
t.Helper()
root := t.TempDir()
files := map[string]string{
"a.c": "int x;\nint y;\n",
"sub/b.h": "#define Z 1",
"sub/vector": "// libc++ header, no extension\n",
"notes.txt": "not C\n",
".git/HEAD": "ref: x.c\n",
".git/obj/x.c": "int planted_in_git;\n",
}
for name, body := range files {
p := filepath.Join(root, filepath.FromSlash(name))
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
t.Fatal(err)
}
}
if err := os.Symlink(filepath.Join(root, "a.c"), filepath.Join(root, "link.c")); err != nil {
t.Fatal(err)
}
return root
}

func run(t *testing.T, args ...string) (string, error) {
t.Helper()
var out bytes.Buffer
cmd := RootCmd()
cmd.SetArgs(args)
cmd.SetOut(&out)
cmd.SetErr(&out)
err := cmd.Execute()
return out.String(), err
}

func TestRootCmdExt(t *testing.T) {
root := tree(t)
out, err := run(t, "--ext", ".c,.h", root)
if err != nil {
t.Fatal(err)
}
// a.c (2 lines, 14 bytes) + sub/b.h (1 line, 11 bytes); .git and the
// symlink are not counted.
if want := "\tfiles 2\tlines 3\tbytes 25\n"; !strings.HasSuffix(out, want) {
t.Fatalf("got %q, want suffix %q", out, want)
}
}

func TestRootCmdAllFilesContains(t *testing.T) {
root := tree(t)
out, err := run(t, "--contains", "int", root, filepath.Join(root, "a.c"))
if err != nil {
t.Fatal(err)
}
for _, want := range []string{
root + "\tfiles 4\tlines 5\tbytes 62\tcontaining \"int\" 2\n",
filepath.Join(root, "a.c") + "\tfiles 1\tlines 2\tbytes 14\tcontaining \"int\" 2\n",
} {
if !strings.Contains(out, want) {
t.Errorf("output lacks %q:\n%s", want, out)
}
}
}

// Nothing matched is a reading error, never a count of zero.
func TestRootCmdNoFile(t *testing.T) {
if _, err := run(t, "--ext", ".rs", tree(t)); err == nil || !strings.Contains(err.Error(), "no file") {
t.Fatalf("err = %v, want a no-file error", err)
}
}

func TestRootCmdMissing(t *testing.T) {
if _, err := run(t, filepath.Join(t.TempDir(), "absent")); err == nil {
t.Fatal("want an error for a missing path")
}
}

func TestRootCmdUnreadable(t *testing.T) {
root := tree(t)
p := filepath.Join(root, "a.c")
if err := os.Chmod(p, 0); err != nil {
t.Fatal(err)
}
defer os.Chmod(p, 0o644)
if _, err := os.ReadFile(p); err == nil {
t.Skip("running with privileges that ignore file modes")
}
if _, err := run(t, root); err == nil {
t.Fatal("want an error for an unreadable file")
}
}

func TestMain_(t *testing.T) {
saved, savedExit, savedOut := os.Args, osExit, os.Stdout
defer func() { os.Args, osExit, os.Stdout = saved, savedExit, savedOut }()
devnull, err := os.OpenFile(os.DevNull, os.O_WRONLY, 0)
if err != nil {
t.Fatal(err)
}
defer devnull.Close()
os.Stdout = devnull
code := -1
osExit = func(c int) { code = c }

os.Args = []string{"srccount", tree(t)}
main()
if code != -1 {
t.Fatalf("exit %d on a good tree", code)
}
os.Args = []string{"srccount"}
main()
if code != 1 {
t.Fatalf("exit %d without an argument, want 1", code)
}
}
Loading
Loading