PathrockNetwork Gno Explorer
HomeBlocksTransactionsTokensRealmsPackagesValidatorsAnalytics

PathrockNetwork Gno Explorer — an independent explorer for Gno.land Mainnet (gnoland-1), operated by PathrockNetwork. Not an official Gno.land service.

gnowebarchive RPC

gno.land/p/moul/x/vm/bf/v0

Package
Open in gnoweb ↗

Overview

Kind
Pure package
Name
v0
Namespace
moul / x / vm / bf
Files
9 (README)(gnomod.toml)
Exported functions
n/a — not supported for pure packages by the node (vm/qfuncs)
Module
gno.land/p/moul/x/vm/bf/v0
gno
0.9

Files (9)

  • README.mdmarkdown
  • gnomod.tomltoml
  • bf.gnogno
  • compile.gnogno
  • machine.gnogno
  • bf_test.gnogno
  • ladder_test.gnogno
  • machine_test.gnogno
  • micro_test.gnogno
  • compile.gnogno
    1package bf23import (4	"errors"56	"gno.land/p/nt/ufmt/v0"7)8

    Functions

    not supported for pure packages by the node (vm/qfuncs)

    Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.

    9// TapeSize is the tape length of a compiled [Machine]. It matches
    10// [NaiveTapeSize] so that the ladder compares like with like: a rung that
    11// changed the semantics would not be a rung, it would be a different program.
    12const TapeSize = 30000
    13
    14// MaxSource bounds the program a realm will accept. Compilation is linear in
    15// the source, but the op array and every snapshot of it are consensus state,
    16// so the size has to be bounded somewhere the caller can see.
    17const MaxSource = 64 * 1024
    18
    19// ErrSourceTooLong is returned by [Compile] for a program above [MaxSource].
    20var ErrSourceTooLong = errors.New("bf: source too long")
    21
    22// Level selects how far up the optimization ladder [Compile] goes.
    23//
    24// Every level produces the same output for the same program: the levels exist
    25// so the cost of each optimization can be measured in isolation, which is the
    26// published result this package is really for. Rung 0 of the ladder is
    27// [Execute] and has no compiler at all.
    28type Level int
    29
    30const (
    31 // LevelJumps is rung 1: one op per source byte, with the jump targets
    32 // resolved at compile time. This is the rung that kills the brace
    33 // rescan, which in [Execute] costs O(loop body) on every iteration.
    34 LevelJumps Level = iota
    35
    36 // LevelFuse is rung 2: runs of identical operators collapse, so
    37 // "+++++" is one add-5 and ">>>" is one move-3. Real programs are
    38 // mostly runs.
    39 LevelFuse
    40
    41 // LevelIdioms is rung 3: a loop whose body only moves and adds, and
    42 // which returns to where it started while decrementing the current
    43 // cell by one, is replaced by the multiply-adds it performs plus a
    44 // clear. "[-]" becomes one op, "[->+<]" becomes two.
    45 LevelIdioms
    46)
    47
    48// Default is the level [CompileDefault] uses, and the one a realm should
    49// want: every optimization this package knows, same semantics.
    50const Default = LevelIdioms
    51
    52// String names the level, for the ladder table and for realm output.
    53func (l Level) String() string {
    54 switch l {
    55 case LevelJumps:
    56 return "jumps"
    57 case LevelFuse:
    58 return "fuse"
    59 case LevelIdioms:
    60 return "idioms"
    61 }
    62 return "unknown"
    63}
    64
    65type opcode byte
    66
    67const (
    68 opAdd opcode = iota // tape[p] += arg (arg normalized to 0..255)
    69 opMove // p += arg
    70 opOut // write tape[p] to the host
    71 opIn // read one input byte into tape[p]
    72 opJmpZ // if tape[p] == 0 { pc = arg }
    73 opJmpNZ // if tape[p] != 0 { pc = arg }
    74 opSet // tape[p] = byte(arg)
    75 opAddMul // tape[p+off] += tape[p] * byte(arg)
    76 opScan // while tape[p] != 0 { p += arg }
    77 opHalt // end of program
    78)
    79
    80// op is one compiled instruction. It is a flat struct on purpose: the
    81// execution loop indexes an array of these, and an op that owned a slice
    82// would allocate in the hot path.
    83type op struct {
    84 code opcode
    85 arg int
    86 off int
    87}
    88
    89// Program is compiled Brainfuck, ready to run on a [Machine].
    90type Program struct {
    91 ops []op
    92 src string
    93 level Level
    94}
    95
    96// Source returns the program text the [Program] was compiled from.
    97func (p *Program) Source() string { return p.src }
    98
    99// Level returns the ladder rung this program was compiled at.
    100func (p *Program) Level() Level { return p.level }
    101
    102// Len returns the number of compiled ops, including the trailing halt. It is
    103// the number worth quoting next to the source length: the ratio is exactly
    104// what the ladder buys.
    105func (p *Program) Len() int { return len(p.ops) }
    106
    107// CompileDefault compiles at [Default].
    108func CompileDefault(src string) (*Program, error) { return Compile(src, Default) }
    109
    110// Compile turns Brainfuck source into a [Program] at the requested ladder
    111// level. Everything that is not one of the eight operators is a comment, as
    112// the language requires.
    113//
    114// It fails on unbalanced brackets rather than trusting the runtime to notice,
    115// which is the first thing that separates it from [Execute]: a malformed
    116// program is rejected before anybody pays to run it.
    117func Compile(src string, lvl Level) (*Program, error) {
    118 if len(src) > MaxSource {
    119 return nil, ErrSourceTooLong
    120 }
    121 if lvl < LevelJumps || lvl > LevelIdioms {
    122 return nil, ufmt.Errorf("bf: unknown level %d", int(lvl))
    123 }
    124
    125 ops := []op{}
    126 opens := []int{} // pc of each unclosed opJmpZ
    127
    128 for i := 0; i < len(src); i++ {
    129 c := src[i]
    130 switch c {
    131 case '+', '-':
    132 delta := 1
    133 if c == '-' {
    134 delta = -1
    135 }
    136 if lvl >= LevelFuse {
    137 n := 1
    138 for i+n < len(src) && (src[i+n] == '+' || src[i+n] == '-') {
    139 if src[i+n] == '+' {
    140 delta++
    141 } else {
    142 delta--
    143 }
    144 n++
    145 }
    146 i += n - 1
    147 }
    148 if a := normAdd(delta); a != 0 {
    149 ops = append(ops, op{code: opAdd, arg: a})
    150 }
    151 case '>', '<':
    152 delta := 1
    153 if c == '<' {
    154 delta = -1
    155 }
    156 if lvl >= LevelFuse {
    157 n := 1
    158 for i+n < len(src) && (src[i+n] == '>' || src[i+n] == '<') {
    159 if src[i+n] == '>' {
    160 delta++
    161 } else {
    162 delta--
    163 }
    164 n++
    165 }
    166 i += n - 1
    167 }
    168 if d := normMove(delta); d != 0 {
    169 ops = append(ops, op{code: opMove, arg: d})
    170 }
    171 case '.':
    172 ops = append(ops, op{code: opOut})
    173 case ',':
    174 ops = append(ops, op{code: opIn})
    175 case '[':
    176 opens = append(opens, len(ops))
    177 ops = append(ops, op{code: opJmpZ})
    178 case ']':
    179 if len(opens) == 0 {
    180 return nil, ufmt.Errorf("bf: unmatched ']' at byte %d", i)
    181 }
    182 open := opens[len(opens)-1]
    183 opens = opens[:len(opens)-1]
    184
    185 if lvl >= LevelIdioms {
    186 if idiom, ok := simpleLoop(ops[open+1:]); ok {
    187 ops = append(ops[:open], idiom...)
    188 continue
    189 }
    190 }
    191 // pc lands on the op after the jump, so a jump target is
    192 // the index of the partner op itself.
    193 ops = append(ops, op{code: opJmpNZ, arg: open})
    194 ops[open].arg = len(ops) - 1
    195 }
    196 }
    197 if len(opens) != 0 {
    198 return nil, ufmt.Errorf("bf: %d unmatched '['", len(opens))
    199 }
    200
    201 ops = append(ops, op{code: opHalt})
    202 return &Program{ops: ops, src: src, level: lvl}, nil
    203}
    204
    205// normAdd folds a signed delta into the 0..255 the byte tape actually sees,
    206// so the execution loop never has to convert a negative int to a byte.
    207func normAdd(d int) int {
    208 d %= 256
    209 if d < 0 {
    210 d += 256
    211 }
    212 return d
    213}
    214
    215// normMove folds a pointer delta into one lap of the tape. Moving right
    216// TapeSize times is a no-op on a wrapping tape, in the naive interpreter as
    217// much as here, so collapsing it changes nothing but the op count.
    218func normMove(d int) int {
    219 d %= TapeSize
    220 if d < 0 {
    221 d += TapeSize
    222 }
    223 if d > TapeSize/2 {
    224 d -= TapeSize
    225 }
    226 return d
    227}
    228
    229// cell is one offset the body of a simple loop writes to.
    230type cell struct {
    231 off int
    232 delta int
    233}
    234
    235// simpleLoop decides whether a loop body can be replaced by straight-line
    236// code, and returns that code.
    237//
    238// Two shapes qualify, and they are the two that dominate real programs:
    239//
    240// - The body only moves, and ends somewhere other than where it started:
    241// that is a scan, "[>]" walking to the next zero cell.
    242// - The body only moves and adds, ends where it started, and takes exactly
    243// one off the current cell: that is a multiply-add. The loop runs
    244// tape[p] times, so every other cell it touches gains its delta times
    245// tape[p], and the current cell ends at zero. "[-]" is the degenerate
    246// case with no other cells.
    247//
    248// Anything else, including any body containing I/O or a nested loop that was
    249// itself rewritten, is left alone. Being conservative here costs a few ops in
    250// rare programs and is the only reason this rewrite is safe at all: a loop
    251// whose current cell does not reach zero in steps of one is not guaranteed to
    252// terminate, and constant-folding it would change the program.
    253func simpleLoop(body []op) ([]op, bool) {
    254 cursor := 0
    255 cells := []cell{}
    256 adds := 0
    257
    258 for _, o := range body {
    259 switch o.code {
    260 case opMove:
    261 cursor += o.arg
    262 case opAdd:
    263 adds++
    264 // Carry the delta as a signed value; it is normalized
    265 // back into 0..255 when it is emitted.
    266 d := o.arg
    267 if d > 128 {
    268 d -= 256
    269 }
    270 cells = addCell(cells, cursor, d)
    271 default:
    272 return nil, false
    273 }
    274 }
    275
    276 if cursor != 0 {
    277 // Net movement: only a pure scan qualifies, and only when the
    278 // step is not zero (which normMove already guarantees here).
    279 if adds != 0 {
    280 return nil, false
    281 }
    282 return []op{{code: opScan, arg: cursor}}, true
    283 }
    284
    285 // Balanced. The current cell must fall by exactly one per iteration,
    286 // or the loop is not a counted multiply-add.
    287 if delta(cells, 0) != -1 {
    288 return nil, false
    289 }
    290
    291 out := []op{}
    292 for _, c := range cells {
    293 if c.off == 0 || c.delta == 0 {
    294 continue
    295 }
    296 out = append(out, op{code: opAddMul, arg: normAdd(c.delta), off: c.off})
    297 }
    298 out = append(out, op{code: opSet, arg: 0})
    299 return out, true
    300}
    301
    302// addCell accumulates a delta at an offset, keeping cells sorted by offset so
    303// that the op stream a program compiles to is identical on every node.
    304func addCell(cells []cell, off, d int) []cell {
    305 for i := range cells {
    306 if cells[i].off == off {
    307 cells[i].delta += d
    308 return cells
    309 }
    310 if cells[i].off > off {
    311 cells = append(cells, cell{})
    312 copy(cells[i+1:], cells[i:])
    313 cells[i] = cell{off: off, delta: d}
    314 return cells
    315 }
    316 }
    317 return append(cells, cell{off: off, delta: d})
    318}
    319
    320func delta(cells []cell, off int) int {
    321 for _, c := range cells {
    322 if c.off == off {
    323 return c.delta
    324 }
    325 }
    326 return 0
    327}
    328