Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: upgrade macro evaluation strategy to use nimscripter #37

Merged
merged 20 commits into from
Jun 9, 2022
Merged
Show file tree
Hide file tree
Changes from 5 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
43 changes: 36 additions & 7 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,55 @@ on:
jobs:
job1:
name: Nim Tests
runs-on: ubuntu-18.04
container: nimlang/nim:1.2.4-ubuntu-regular@sha256:02a555518a05c354ccb2627940f6138fca1090d198e5a0eb60936c03a4875c69
runs-on: ubuntu-22.04
ynfle marked this conversation as resolved.
Show resolved Hide resolved

steps:
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b

- uses: jiro4989/setup-nim-action@5efcb0ac6189962c61098fcbe9a0765c1d81467b
ynfle marked this conversation as resolved.
Show resolved Hide resolved
with:
nim-version: '1.6.6'

- name: Cache nimble
id: cache-nimble
uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09
ynfle marked this conversation as resolved.
Show resolved Hide resolved
with:
path: ~/.nimble
key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }}

- name: Compile and run tests with `nimble test`
run: "nimble test"
run: "nimble test -Y"

ynfle marked this conversation as resolved.
Show resolved Hide resolved

job2:
name: Smoke test
runs-on: ubuntu-18.04
container: nimlang/nim:1.2.4-ubuntu-regular@sha256:02a555518a05c354ccb2627940f6138fca1090d198e5a0eb60936c03a4875c69
runs-on: ubuntu-22.04
ynfle marked this conversation as resolved.
Show resolved Hide resolved

steps:
- uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b

- uses: jiro4989/setup-nim-action@5efcb0ac6189962c61098fcbe9a0765c1d81467b
ynfle marked this conversation as resolved.
Show resolved Hide resolved
with:
nim-version: '1.6.6'

- name: Cache nimble
id: cache-nimble
uses: actions/cache@48af2dc4a9e8278b89d7fa154b955c30c6aaab09
ynfle marked this conversation as resolved.
Show resolved Hide resolved
with:
path: ~/.nimble
key: ${{ runner.os }}-nimble-${{ hashFiles('*.nimble') }}

- name: "Install nimble dependencies"
run: "nimble install -y -d"
if: steps.cache-nimble.outputs.cache-hit != 'true'
ynfle marked this conversation as resolved.
Show resolved Hide resolved

- name: "Compile representer"
run: "nimble c -d:release src/representer"

- name: "Make representation of `two-fer`"
run: "bin/run.sh two-fer ${PWD}/tests/cases/example-two-fer/ ${PWD}/tests/cases/example-two-fer/"

- name: "Check diffs"
run: |
diff tests/cases/example-two-fer/mapping.json tests/cases/example-two-fer/expected/mapping.json
diff tests/cases/example-two-fer/representation.txt tests/cases/example-two-fer/expected/representation.txt
diff -y tests/cases/example-two-fer/mapping.json tests/cases/example-two-fer/expected/mapping.json
diff -y tests/cases/example-two-fer/representation.txt tests/cases/example-two-fer/expected/representation.txt
3 changes: 2 additions & 1 deletion bin/run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ if [ -z "$1" ] || [ -z "$2" ] || [ -z "$3" ]; then
echo "slug, solution directory and output directory must be present"
exit 1
fi
nim c -f --outdir:bin/ -d:slug="$1" -d:inDir="$2" -d:outDir="$3" src/representer

bin/representer --slug="$1" --input-dir="$2" --output-dir="$3" --print
ee7 marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions nim.cfg
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
--path:"$nim"
ee7 marked this conversation as resolved.
Show resolved Hide resolved
--verbosity=0
--hint[Processing]:off
--styleCheck:hint
Expand Down
4 changes: 3 additions & 1 deletion nim_representer.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ binDir = "bin"

# Dependencies

requires "nim >= 1.0.0"
requires "nim >= 1.6.4"
requires "nimscripter == 1.0.14"
requires "docopt == 0.6.8"
ynfle marked this conversation as resolved.
Show resolved Hide resolved
56 changes: 36 additions & 20 deletions src/representer.nim
Original file line number Diff line number Diff line change
@@ -1,26 +1,42 @@
import macros, os, sequtils, strutils
import representer/[mapping, normalizations]
import std/[json, os, strformat, strutils]
import nimscripter
import representer/[mapping, types]
import docopt
ynfle marked this conversation as resolved.
Show resolved Hide resolved

proc switchKeysValues*(map: IdentMap): OrderedTable[string, NormalizedIdent] =
toSeq(map.pairs).mapIt((it[1], it[0])).toOrderedTable
const doc = """
Exercism nim representation normalizer.

proc createRepresentation*(fileName: string): tuple[tree: NimNode, map: IdentMap] =
var map: IdentMap
let code = parseStmt fileName.staticRead
result = (tree: code.normalizeStmtList(map), map: map)
Usage:
representer --slug=<slug> --input-dir=<in-dir> [--output-dir=<out-dir>] [--print]

Options:
-h --help Show this help message.
-v, --version Display version.
-p, --print Print the results.
-s <slug>, --slug=<slug> The exercise slug.
-i <in-dir>, --input-dir=<in-dir> The directory of the submission and exercise files.
-o <out-dir>, --output-dir=<out-dir> The directory to output to.
This is optional and will then output to stdout.
ynfle marked this conversation as resolved.
Show resolved Hide resolved
"""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking comment: I personally slightly prefer to indent this kind of const multi-line string, and then use """.dedent(2).


const inDir {.strdefine.} = ""
const outDir {.strdefine.} = ""
const slug {.strdefine.} = ""
const underSlug = slug.replace('-', '_')
proc getFileContents*(fileName: string): string = readFile fileName
ynfle marked this conversation as resolved.
Show resolved Hide resolved

func underSlug(s: string): string = s.replace('-', '_')
ee7 marked this conversation as resolved.
Show resolved Hide resolved

proc main() =
let args = docopt(doc)
ynfle marked this conversation as resolved.
Show resolved Hide resolved
let intr = loadScript(NimScriptPath("src/representer/loader.nims"))
let (tree, map) = intr.invoke(
getTestableRepresentation,
getFileContents(&"""{args["--input-dir"]}/{($args["--slug"]).underSlug}.nim"""), true,
ynfle marked this conversation as resolved.
Show resolved Hide resolved
returnType = SerializedRepresentation
)
if args["--output-dir"]:
let outDir = $args["--output-dir"]
writeFile outDir / "mapping.json", $map.parseJson
writeFile outDir / "representation.txt", tree
if not args["--output-dir"] or args["--print"]:
echo &"{tree = }\n{map.parseJson.pretty = }"

when isMainModule:
import json
static:
let (tree, map) = createRepresentation(inDir / underSlug & ".nim")
let finalMapping = map.switchKeysValues
echo (%*{"map": finalMapping, "tree": tree.repr}).pretty
when defined(outDir):
writeFile(outDir / "representation.txt", tree.repr)
writeFile(outDir / "mapping.json", $(%finalMapping))
main()
14 changes: 14 additions & 0 deletions src/representer/loader.nims
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import std/[json, macros]
import mapping
import normalizations
import types
ynfle marked this conversation as resolved.
Show resolved Hide resolved

proc createRepresentation*(file: string): tuple[tree: NimNode, map: IdentMap] =
var map: IdentMap
let code = parseStmt(file)
result = (tree: code.normalizeStmtList(map), map: map)
ynfle marked this conversation as resolved.
Show resolved Hide resolved

proc getTestableRepresentation*(contents: string, switch = false): SerializedRepresentation =
let (tree, map) = createRepresentation(contents)
result = (repr tree, $(if switch: %map.switchKeysValues else: %map))

ynfle marked this conversation as resolved.
Show resolved Hide resolved
4 changes: 3 additions & 1 deletion src/representer/normalizations.nim
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
import algorithm, macros, strformat, sequtils, strutils, std/with
import mapping

{.experimental: "strictFuncs".}

proc normalizeStmtList*(code: NimNode, map: var IdentMap): NimNode
ynfle marked this conversation as resolved.
Show resolved Hide resolved
proc normalizeValue(value: NimNode, map: var IdentMap): NimNode

Expand Down Expand Up @@ -35,7 +37,7 @@ proc constructFmtStr(ast: NimNode, map: var IdentMap): string =
proc normalizeCall(call: NimNode, map: var IdentMap): NimNode =
result =
if call.kind != nnkInfix and (call[0] == "fmt".ident or call[0] == "&".ident):
let fmtAst = getAst(fmt(call[1]))
let fmtAst = getAst(&(call[1]))
let strToFmt = fmtAst[1..^2].mapIt(
if $it[0][0] == "add":
$it[2]
Expand Down
14 changes: 14 additions & 0 deletions src/representer/types.nim
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import std/[sequtils, tables]
import mapping

type
Representation* = tuple
tree: string
map: IdentMap
SerializedRepresentation* = tuple
tree: string
map: string

proc switchKeysValues*(map: IdentMap): OrderedTable[string, NormalizedIdent] =
toSeq(map.pairs).mapIt((it[1], it[0])).toOrderedTable
ynfle marked this conversation as resolved.
Show resolved Hide resolved

ynfle marked this conversation as resolved.
Show resolved Hide resolved
149 changes: 58 additions & 91 deletions tests/tnormalizations.nim
Original file line number Diff line number Diff line change
@@ -1,86 +1,52 @@
import representer/[mapping, normalizations]
import macros, sequtils, strutils, unittest


macro setup(test, code: untyped): untyped =
var map: IdentMap
let tree = code.normalizeStmtList(map)
let tableConstr = nnkTableConstr.newTree.add(toSeq(map.pairs).mapIt(
nnkExprColonExpr.newTree(
newDotExpr(it[0].string.newStrLitNode, "NormalizedIdent".ident),
it[1].newStrLitNode
)
))

let tableInit =
if tableConstr.len != 0:
newDotExpr(
tableConstr,
"toOrderedTable".ident
)
else:
newEmptyNode()

newStmtList(
nnkLetSection.newTree(
nnkIdentDefs.newTree(
nnkPragmaExpr.newTree(
ident "tree",
nnkPragma.newTree(ident "used")
),
newEmptyNode(),
newLit tree.repr
),
),
nnkVarSection.newTree(
nnkIdentDefs.newTree(
nnkPragmaExpr.newTree(
ident "map",
nnkPragma.newTree(ident "used")
),
"IdentMap".ident,
tableInit
)
),

newCall("check", test)
)
import std/[json, strutils, unittest]
import nimscripter
ynfle marked this conversation as resolved.
Show resolved Hide resolved
import representer/types

let
intr = loadScript(NimScriptPath "src/representer/loader.nims")

proc getRepresentation(t: string): tuple[tree: string, map: JsonNode] =
let (tree, map) = intr.invoke(getTestableRepresentation, t, false, returnType = SerializedRepresentation)
result = (tree, map.parseJson)

suite "End to end":
test "Just one `let` statement":
setup(map["x".NormalizedIdent] == "placeholder_0" and map.len == 1):
let x = 1
let (_, map) = getRepresentation """let x = 1"""
check:
map["x"].getStr == "placeholder_0"
map.len == 1

test "All features":
let (_, map) = getRepresentation"""type
ynfle marked this conversation as resolved.
Show resolved Hide resolved
Dollar = distinct int

setup(map.len == 11):
type
Dollar = distinct int
proc testProc(name: string = "", hello: int) =

proc testProc(name: string = "", hello: int) =
discard name & $hello

discard name & $hello
let
x = 1
y = 2
z = y + x

let
x = 1
y = 2
z = y + x
var
dollar: Dollar

var
dollar: Dollar
const
euro = 100.Dollar

const
euro = 100.Dollar
testProc(name = $x, hello = y)
testproC x

testProc(name = $x, hello = y)
testproC x
macro testMacro(code: untyped): untyped = discard
template testTemplate(code: untyped): untyped = discard"""

macro testMacro(code: untyped): untyped = discard
template testTemplate(code: untyped): untyped = discard
check map.len == 11

test "No params, return type or statements":
setup(tree.strip == "proc placeholder_0*() =\n discard".strip):
proc helloWorld* = discard
let (tree, _) = getRepresentation """proc helloWorld* = discard"""

check tree.strip == "proc placeholder_0*() =\n discard".strip

test "All the things":
const expected = """import
Expand All @@ -101,27 +67,27 @@ proc placeholder_5*(placeholder_3: int; placeholder_4 = "seventeen"): string =

echo(placeholder_0.placeholder_5)
echo(placeholder_5(placeholder_3 = 1, placeholder_4 = "how old am I?"))"""
setup(tree.strip == expected.strip):
import strutils, algorithm, macros as m
let (tree, _) = getRepresentation """import strutils, algorithm, macros as m

let
x = 1
y = ($x).strip.replace("\n", $x)
let
x = 1
y = ($x).strip.replace("\n", $x)

proc testStdout*() =
echo "testing stdout"
proc testStdout*() =
echo "testing stdout"

testStdout()
testStdout()

proc helloWorld*(name: int, age = "seventeen"): string =
let years = name - x
let fullName = y & age
let id = $yEARs & fuLLNamE
id
proc helloWorld*(name: int, age = "seventeen"): string =
let years = name - x
let fullName = y & age
let id = $yEARs & fuLLNamE
id

echo x.helloWorld
echo x.helloWorld

echo hELLOWORLD(name = 1, age = "how old am I?")
echo hELLOWORLD(name = 1, age = "how old am I?")"""
check tree.strip == expected.string

suite "Test specific functionality":
let expected = """import
Expand All @@ -130,15 +96,16 @@ suite "Test specific functionality":
proc placeholder_1*(placeholder_0 = "you"): string =
fmt"One for {placeholder_0}, one for me.""""
test "fmt strings":
setup(tree.strip == expected.strip):
import strformat
let (tree, _) = getRepresentation """import strformat

proc twoFer*(name = "you"): string =
fmt"One for {name}, one for me." """

proc twoFer*(name = "you"): string =
fmt"One for {name}, one for me."
check tree.strip == expected.strip

test "fmt string with `&`":
setup(tree.strip == expected.strip):
import strformat
let (tree, _) = getRepresentation """import strformat

proc twoFer*(name = "you"): string =
&"One for {name}, one for me."
proc twoFer*(name = "you"): string =
&"One for {name}, one for me." """
check expected.strip == tree.strip