Skip to content

Commit

Permalink
AVM: Generate langspec for each version (#5629)
Browse files Browse the repository at this point in the history
  • Loading branch information
jasonpaulos authored Aug 26, 2023
1 parent b19594e commit ca420de
Show file tree
Hide file tree
Showing 26 changed files with 39,453 additions and 142 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ crypto/libs

# doc intermediates
data/transactions/logic/*.md
!data/transactions/logic/TEAL_opcodes*.md

*.pem

Expand Down
156 changes: 78 additions & 78 deletions cmd/opdoc/opdoc.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"encoding/json"
"fmt"
"io"
"math"
"os"
"sort"
"strings"
Expand All @@ -29,8 +30,6 @@ import (
"github.com/algorand/go-algorand/protocol"
)

var docVersion = 10

// OpImmediateNote returns a short string about immediate data which follows the op byte
func opImmediateNoteSyntaxMarkdown(name string, oids []logic.OpImmediateDetails) string {
if len(oids) == 0 {
Expand Down Expand Up @@ -63,11 +62,11 @@ func opImmediateNoteEncoding(opcode byte, oids []logic.OpImmediateDetails) strin
return fmt.Sprintf("0x%02x {%s}", opcode, strings.Join(notes, "}, {"))
}

func opGroupMarkdownTable(names []string, out io.Writer) {
func opGroupMarkdownTable(names []string, out io.Writer, version uint64) {
fmt.Fprint(out, `| Opcode | Description |
| - | -- |
`)
opSpecs := logic.OpsByName[docVersion]
opSpecs := logic.OpsByName[version]
for _, opname := range names {
spec, ok := opSpecs[opname]
if !ok {
Expand Down Expand Up @@ -113,20 +112,20 @@ func integerConstantsTableMarkdown(out io.Writer) {
out.Write([]byte("\n"))
}

func fieldGroupMarkdown(out io.Writer, group *logic.FieldGroup) {
func fieldGroupMarkdown(out io.Writer, group *logic.FieldGroup, version uint64) {
showTypes := false
showVers := false
opVer := uint64(0)
opVer := uint64(math.MaxUint64)
for _, name := range group.Names {
spec, ok := group.SpecByName(name)
// reminder: group.Names can be "sparse" See: logic.TxnaFields
if !ok {
if !ok || spec.Version() > version {
continue
}
if spec.Type().Typed() {
showTypes = true
}
if opVer == uint64(0) {
if opVer == math.MaxUint64 {
opVer = spec.Version()
} else if opVer != spec.Version() {
showVers = true
Expand All @@ -147,7 +146,7 @@ func fieldGroupMarkdown(out io.Writer, group *logic.FieldGroup) {
fmt.Fprint(out, headers, widths)
for i, name := range group.Names {
spec, ok := group.SpecByName(name)
if !ok {
if !ok || spec.Version() > version {
continue
}
str := fmt.Sprintf("| %d | %s", i, markdownTableEscape(name))
Expand Down Expand Up @@ -212,7 +211,7 @@ func stackMarkdown(op *logic.OpSpec) string {
return out + "\n"
}

func opToMarkdown(out io.Writer, op *logic.OpSpec, groupDocWritten map[string]bool) (err error) {
func opToMarkdown(out io.Writer, op *logic.OpSpec, groupDocWritten map[string]bool, version uint64) (err error) {

deets := logic.OpImmediateDetailsFromSpec(*op)

Expand All @@ -230,26 +229,9 @@ func opToMarkdown(out io.Writer, op *logic.OpSpec, groupDocWritten map[string]bo
fmt.Fprintf(out, "\n## %s\n\n%s%s\n%s", op.Name, syntax, encoding, stackEffects)

fmt.Fprintf(out, "- %s\n", logic.OpDoc(op.Name))
// if cost changed with versions print all of them
costs := logic.OpAllCosts(op.Name)
if len(costs) > 1 {
fmt.Fprintf(out, "- **Cost**:\n")
for _, cost := range costs {
if cost.From == cost.To {
fmt.Fprintf(out, " - %s (v%d)\n", cost.Cost, cost.To)
} else {
if cost.To < docVersion {
fmt.Fprintf(out, " - %s (v%d - v%d)\n", cost.Cost, cost.From, cost.To)
} else {
fmt.Fprintf(out, " - %s (since v%d)\n", cost.Cost, cost.From)
}
}
}
} else {
cost := costs[0].Cost
if cost != "1" {
fmt.Fprintf(out, "- **Cost**: %s\n", cost)
}
cost := op.DocCost(version)
if cost != "1" {
fmt.Fprintf(out, "- **Cost**: %s\n", cost)
}
if op.Version > 1 {
fmt.Fprintf(out, "- Availability: v%d\n", op.Version)
Expand All @@ -262,7 +244,7 @@ func opToMarkdown(out io.Writer, op *logic.OpSpec, groupDocWritten map[string]bo
group := op.OpDetails.Immediates[i].Group
if group != nil && group.Doc != "" && !groupDocWritten[group.Name] {
fmt.Fprintf(out, "\n### %s\n\n%s\n\n", group.Name, group.Doc)
fieldGroupMarkdown(out, group)
fieldGroupMarkdown(out, group, version)
groupDocWritten[group.Name] = true
}
}
Expand All @@ -273,17 +255,20 @@ func opToMarkdown(out io.Writer, op *logic.OpSpec, groupDocWritten map[string]bo
return nil
}

func opsToMarkdown(out io.Writer) (err error) {
out.Write([]byte("# Opcodes\n\nOps have a 'cost' of 1 unless otherwise specified.\n\n"))
opSpecs := logic.OpcodesByVersion(uint64(docVersion))
func opsToMarkdown(out io.Writer, version uint64) error {
_, err := out.Write([]byte(fmt.Sprintf("# v%d Opcodes\n\nOps have a 'cost' of 1 unless otherwise specified.\n\n", version)))
if err != nil {
return err
}
opSpecs := logic.OpcodesByVersion(version)
written := make(map[string]bool)
for i := range opSpecs {
err = opToMarkdown(out, &opSpecs[i], written)
err := opToMarkdown(out, &opSpecs[i], written, version)
if err != nil {
return
return err
}
}
return
return nil
}

// OpRecord is a consolidated record of things about an Op
Expand All @@ -297,6 +282,8 @@ type OpRecord struct {
ArgEnum []string `json:",omitempty"`
ArgEnumTypes []string `json:",omitempty"`

DocCost string

Doc string
DocExtra string `json:",omitempty"`
ImmediateNote []logic.OpImmediateDetails `json:",omitempty"`
Expand Down Expand Up @@ -342,7 +329,7 @@ func (nt namedType) boundString() string {

// LanguageSpec records the ops of the language at some version
type LanguageSpec struct {
EvalMaxVersion int
Version uint64
LogicSigVersion uint64
NamedTypes []namedType
Ops []OpRecord
Expand All @@ -369,67 +356,68 @@ func typeStrings(types logic.StackTypes) []string {
return out
}

func fieldsAndTypes(group logic.FieldGroup) ([]string, []string) {
func fieldsAndTypes(group logic.FieldGroup, version uint64) ([]string, []string) {
// reminder: group.Names can be "sparse" See: logic.TxnaFields
fields := make([]string, 0, len(group.Names))
types := make([]logic.StackType, 0, len(group.Names))
for _, name := range group.Names {
if spec, ok := group.SpecByName(name); ok {
if spec, ok := group.SpecByName(name); ok && spec.Version() <= version {
fields = append(fields, name)
types = append(types, spec.Type())
}
}
return fields, typeStrings(types)
}

func argEnums(name string) ([]string, []string) {
func argEnums(name string, version uint64) ([]string, []string) {
// reminder: this needs to be manually updated every time
// a new opcode is added with an associated FieldGroup
// it'd be nice to have this auto-update
switch name {
case "txn", "gtxn", "gtxns", "itxn", "gitxn":
return fieldsAndTypes(logic.TxnFields)
return fieldsAndTypes(logic.TxnFields, version)
case "itxn_field":
// itxn_field does not *return* a type depending on its immediate. It *takes* it.
// but until a consumer cares, ArgEnumTypes will be overloaded for that meaning.
return fieldsAndTypes(logic.ItxnSettableFields)
return fieldsAndTypes(logic.ItxnSettableFields, version)
case "global":
return fieldsAndTypes(logic.GlobalFields)
return fieldsAndTypes(logic.GlobalFields, version)
case "txna", "gtxna", "gtxnsa", "txnas", "gtxnas", "gtxnsas", "itxna", "gitxna":
return fieldsAndTypes(logic.TxnArrayFields)
return fieldsAndTypes(logic.TxnArrayFields, version)
case "asset_holding_get":
return fieldsAndTypes(logic.AssetHoldingFields)
return fieldsAndTypes(logic.AssetHoldingFields, version)
case "asset_params_get":
return fieldsAndTypes(logic.AssetParamsFields)
return fieldsAndTypes(logic.AssetParamsFields, version)
case "app_params_get":
return fieldsAndTypes(logic.AppParamsFields)
return fieldsAndTypes(logic.AppParamsFields, version)
case "acct_params_get":
return fieldsAndTypes(logic.AcctParamsFields)
return fieldsAndTypes(logic.AcctParamsFields, version)
case "block":
return fieldsAndTypes(logic.BlockFields)
return fieldsAndTypes(logic.BlockFields, version)
case "json_ref":
return fieldsAndTypes(logic.JSONRefTypes)
return fieldsAndTypes(logic.JSONRefTypes, version)
case "base64_decode":
return fieldsAndTypes(logic.Base64Encodings)
return fieldsAndTypes(logic.Base64Encodings, version)
case "vrf_verify":
return fieldsAndTypes(logic.VrfStandards)
return fieldsAndTypes(logic.VrfStandards, version)
case "ecdsa_pk_recover", "ecdsa_verify", "ecdsa_pk_decompress":
return fieldsAndTypes(logic.EcdsaCurves)
return fieldsAndTypes(logic.EcdsaCurves, version)
default:
return nil, nil
}
}

func buildLanguageSpec(opGroups map[string][]string, namedTypes []namedType) *LanguageSpec {
opSpecs := logic.OpcodesByVersion(uint64(docVersion))
func buildLanguageSpec(opGroups map[string][]string, namedTypes []namedType, version uint64) *LanguageSpec {
opSpecs := logic.OpcodesByVersion(version)
records := make([]OpRecord, len(opSpecs))
for i, spec := range opSpecs {
records[i].Opcode = spec.Opcode
records[i].Name = spec.Name
records[i].Args = typeStrings(spec.Arg.Types)
records[i].Returns = typeStrings(spec.Return.Types)
records[i].Size = spec.OpDetails.Size
records[i].ArgEnum, records[i].ArgEnumTypes = argEnums(spec.Name)
records[i].DocCost = spec.DocCost(version)
records[i].ArgEnum, records[i].ArgEnumTypes = argEnums(spec.Name, version)
records[i].Doc = strings.ReplaceAll(logic.OpDoc(spec.Name), "<br />", "\n")
records[i].DocExtra = logic.OpDocExtra(spec.Name)
records[i].ImmediateNote = logic.OpImmediateDetailsFromSpec(spec)
Expand All @@ -438,7 +426,7 @@ func buildLanguageSpec(opGroups map[string][]string, namedTypes []namedType) *La
}

return &LanguageSpec{
EvalMaxVersion: docVersion,
Version: version,
LogicSigVersion: config.Consensus[protocol.ConsensusCurrentVersion].LogicSigVersion,
NamedTypes: namedTypes,
Ops: records,
Expand All @@ -448,30 +436,26 @@ func buildLanguageSpec(opGroups map[string][]string, namedTypes []namedType) *La
func create(file string) *os.File {
f, err := os.Create(file)
if err != nil {
fmt.Fprintf(os.Stderr, "Unable to create '%s': %v", file, err)
fmt.Fprintf(os.Stderr, "Unable to create '%s': %v\n", file, err)
os.Exit(1)
}
return f
}

func main() {
opcodesMd := create("TEAL_opcodes.md")
opsToMarkdown(opcodesMd)
opcodesMd.Close()
const docVersion = uint64(10)

opGroups := make(map[string][]string, len(logic.OpSpecs))
for grp, names := range logic.OpGroups {
fname := fmt.Sprintf("%s.md", grp)
fname = strings.ReplaceAll(fname, " ", "_")
fout := create(fname)
opGroupMarkdownTable(names, fout)
opGroupMarkdownTable(names, fout, docVersion)
fout.Close()
for _, opname := range names {
opGroups[opname] = append(opGroups[opname], grp)
}
}
constants := create("named_integer_constants.md")
integerConstantsTableMarkdown(constants)
constants.Close()

named := make([]namedType, 0, len(logic.AllStackTypes))
for abbr, t := range logic.AllStackTypes {
Expand All @@ -484,6 +468,10 @@ func main() {
}
sort.Slice(named, func(i, j int) bool { return named[i].Name > named[j].Name })

constants := create("named_integer_constants.md")
integerConstantsTableMarkdown(constants)
constants.Close()

namedStackTypes := create("named_stack_types.md")
namedStackTypesMarkdown(namedStackTypes, named)
namedStackTypes.Close()
Expand All @@ -494,25 +482,37 @@ func main() {
for _, imm := range spec.OpDetails.Immediates {
if imm.Group != nil && !written[imm.Group.Name] {
out := create(strings.ToLower(imm.Group.Name) + "_fields.md")
fieldGroupMarkdown(out, imm.Group)
fieldGroupMarkdown(out, imm.Group, docVersion)
out.Close()
written[imm.Group.Name] = true
}
}
}

langspecjs := create("langspec.json")
enc := json.NewEncoder(langspecjs)
enc.SetIndent("", " ")
err := enc.Encode(buildLanguageSpec(opGroups, named))
if err != nil {
panic(err.Error())
}
langspecjs.Close()

tealtm := create("teal.tmLanguage.json")
enc = json.NewEncoder(tealtm)
enc := json.NewEncoder(tealtm)
enc.SetIndent("", " ")
enc.Encode(buildSyntaxHighlight())
if err := enc.Encode(buildSyntaxHighlight(docVersion)); err != nil {
fmt.Fprintf(os.Stderr, "error encoding teal.tmLanguage.json: % v\n", err)
os.Exit(1)
}
tealtm.Close()

for v := uint64(1); v <= docVersion; v++ {
langspecjs := create(fmt.Sprintf("langspec_v%d.json", v))
enc := json.NewEncoder(langspecjs)
enc.SetIndent("", " ")
if err := enc.Encode(buildLanguageSpec(opGroups, named, v)); err != nil {
fmt.Fprintf(os.Stderr, "error encoding langspec JSON for version %d: %v\n", v, err)
os.Exit(1)
}
langspecjs.Close()

opcodesMd := create(fmt.Sprintf("TEAL_opcodes_v%d.md", v))
if err := opsToMarkdown(opcodesMd, v); err != nil {
fmt.Fprintf(os.Stderr, "error creating markdown for version %d: %v\n", v, err)
os.Exit(1)
}
opcodesMd.Close()
}
}
12 changes: 9 additions & 3 deletions cmd/opdoc/tmLanguage.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ type pattern struct {
Patterns []pattern `json:"patterns,omitempty"`
}

func buildSyntaxHighlight() *tmLanguage {
func buildSyntaxHighlight(version uint64) *tmLanguage {
tm := tmLanguage{
Schema: "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json",
Name: "Algorand TEAL",
Expand Down Expand Up @@ -126,11 +126,17 @@ func buildSyntaxHighlight() *tmLanguage {
allNamedFields = append(allNamedFields, logic.TxnTypeNames[:]...)
allNamedFields = append(allNamedFields, logic.OnCompletionNames[:]...)
accumulated := make(map[string]bool)
opSpecs := logic.OpcodesByVersion(uint64(docVersion))
opSpecs := logic.OpcodesByVersion(version)
for _, spec := range opSpecs {
for _, imm := range spec.OpDetails.Immediates {
if imm.Group != nil && !accumulated[imm.Group.Name] {
allNamedFields = append(allNamedFields, imm.Group.Names...)
for _, name := range imm.Group.Names {
spec, ok := imm.Group.SpecByName(name)
if !ok || spec.Version() > version {
continue
}
allNamedFields = append(allNamedFields, name)
}
accumulated[imm.Group.Name] = true
}
}
Expand Down
Loading

0 comments on commit ca420de

Please sign in to comment.